diff --git a/README.md b/README.md index 5e4719e..050f854 100644 --- a/README.md +++ b/README.md @@ -314,35 +314,40 @@ Ztick supports Linux containers on both `x86_64` (amd64) and `aarch64` (arm64) a ### AWF CLI -> **Private package** — This feature requires access to the [awf-project/cli](https://github.com/awf-project/cli) private repository via the `gh` CLI. Only the latest version is available (active development). - Installs [AWF CLI](https://github.com/awf-project/cli), an AI Workflow CLI for orchestrating AI coding agents. -Since the repository is private, the binary is downloaded at container startup (not during build) using `gh release download`. This requires the `gh` CLI feature and your host `gh` config mounted into the container. - ```jsonc // devcontainer.json { "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/awf-project/devcontainer-features/awf-cli:1": {} - }, - "mounts": [ - "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,readonly" - ], - "postCreateCommand": "awf-install" + } } ``` -#### How it works +#### Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `version` | string | `latest` | Version to install: `latest` or a specific version (e.g. `0.9.0`) | + +#### Examples + +Pin a specific version: -1. **Build time** (`install.sh`): installs dependencies and places the `awf-install` helper script -2. **Container start** (`postCreateCommand`): `awf-install` uses `gh release download` to fetch the latest binary from the private repo -3. Subsequent rebuilds skip the download if `awf` is already installed +```jsonc +{ + "features": { + "ghcr.io/awf-project/devcontainer-features/awf-cli:1": { + "version": "0.9.0" + } + } +} +``` #### Architecture Support -AWF CLI supports Linux containers on both `x86_64` (amd64) and `aarch64` (arm64) architectures. The feature automatically detects the container architecture and downloads the appropriate binary. +AWF CLI supports Linux containers on both `x86_64` (amd64) and `aarch64` (arm64) architectures. The feature automatically detects the container architecture and downloads the appropriate binary. SHA256 checksums are verified on every install. --- diff --git a/src/awf-cli/devcontainer-feature.json b/src/awf-cli/devcontainer-feature.json index fb40833..ee65076 100644 --- a/src/awf-cli/devcontainer-feature.json +++ b/src/awf-cli/devcontainer-feature.json @@ -1,11 +1,18 @@ { "id": "awf-cli", - "version": "1.0.0", + "version": "2.0.0", "name": "AWF CLI", - "description": "Installs AWF CLI, an AI Workflow CLI for orchestrating AI coding agents. Binary is downloaded at container start via gh CLI (private repository).", - "options": {}, + "description": "Installs AWF CLI, an AI Workflow CLI for orchestrating AI coding agents.", + "documentationURL": "https://github.com/awf-project/cli", + "licenseURL": "https://github.com/awf-project/cli/blob/main/LICENSE", + "options": { + "version": { + "type": "string", + "default": "latest", + "description": "AWF CLI version to install (e.g. '0.9.0' or 'latest')." + } + }, "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils", - "ghcr.io/devcontainers/features/github-cli" + "ghcr.io/devcontainers/features/common-utils" ] } diff --git a/src/awf-cli/install.sh b/src/awf-cli/install.sh index 5653703..90f70d6 100644 --- a/src/awf-cli/install.sh +++ b/src/awf-cli/install.sh @@ -1,82 +1,88 @@ #!/bin/bash set -e -# AWF CLI devcontainer feature -# -# Since awf-project/cli is a private repo, downloading the binary requires -# GitHub authentication. The gh CLI config is mounted from the host at runtime, -# so the actual download happens via postCreateCommand, not at build time. -# -# This script installs the download helper that runs at container start. - -echo "==> AWF CLI feature: setting up deferred install" - -# Ensure curl and tar are available (needed by the download script) -if ! command -v curl &>/dev/null || ! command -v tar &>/dev/null; then - echo "==> Installing missing dependencies..." - apt-get update -y - apt-get install -y --no-install-recommends curl ca-certificates tar -fi - -# Install the download helper script -cat > /usr/local/bin/awf-install << 'SCRIPT' -#!/bin/bash -set -e - -# Skip if already installed -if command -v awf &>/dev/null; then - echo "==> AWF CLI already installed: $(awf --version 2>&1)" - exit 0 -fi - -echo "==> AWF CLI: downloading latest version..." +# --- Options injected by devcontainer feature engine --- +VERSION="${VERSION:-latest}" -# Require gh CLI authenticated -if ! command -v gh &>/dev/null; then - echo "ERROR: gh CLI is required to download AWF CLI from the private repository" - echo "Add ghcr.io/devcontainers/features/github-cli feature to your devcontainer.json" - exit 1 -fi +echo "==> AWF CLI feature: version=${VERSION}" -if ! gh auth status &>/dev/null; then - echo "ERROR: gh CLI is not authenticated" - echo "Mount your host gh config: \"source=\${localEnv:HOME}/.config/gh,target=/home/\${remoteUser}/.config/gh,type=bind,readonly\"" - exit 1 +# Ensure curl, sha256sum, and ca-certificates are available +if ! command -v curl &>/dev/null || ! command -v sha256sum &>/dev/null; then + echo "==> Installing missing dependencies..." + apt-get update -y + apt-get install -y --no-install-recommends curl ca-certificates coreutils + apt-get clean && rm -rf /var/lib/apt/lists/* fi # Detect architecture ARCH=$(uname -m) case "$ARCH" in - x86_64|amd64) GO_ARCH="amd64" ;; - aarch64|arm64) GO_ARCH="arm64" ;; + x86_64|amd64) ARCH_SUFFIX="amd64" ;; + aarch64|arm64) ARCH_SUFFIX="arm64" ;; *) echo "ERROR: unsupported architecture: $ARCH" exit 1 ;; esac +echo "==> Detected architecture: ${ARCH} (${ARCH_SUFFIX})" + +# Resolve version +if [ "$VERSION" = "latest" ]; then + VERSION=$(curl -fsSL "https://api.github.com/repos/awf-project/cli/releases/latest" \ + | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') + if [ -z "$VERSION" ]; then + echo "ERROR: failed to resolve latest AWF CLI version from GitHub API" + exit 1 + fi + echo "==> Resolved latest version: ${VERSION}" +fi + +# Ensure version has 'v' prefix (GitHub tags use v0.9.0 format) +case "$VERSION" in + v*) ;; + *) VERSION="v${VERSION}" ;; +esac -REPO="awf-project/cli" -TARBALL="awf_linux_${GO_ARCH}.tar.gz" +# Download tarball and checksums TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT -# Download latest release asset using gh CLI (handles private repo auth) -echo "==> Downloading ${TARBALL} from ${REPO}..." -gh release download --repo "$REPO" --pattern "$TARBALL" --dir "$TMP_DIR" +ARTIFACT="awf_linux_${ARCH_SUFFIX}.tar.gz" +DOWNLOAD_URL="https://github.com/awf-project/cli/releases/download/${VERSION}/${ARTIFACT}" +CHECKSUMS_URL="https://github.com/awf-project/cli/releases/download/${VERSION}/checksums.txt" -tar -xzf "${TMP_DIR}/${TARBALL}" -C "$TMP_DIR" +echo "==> Downloading ${DOWNLOAD_URL}..." +curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${ARTIFACT}" -if [ ! -f "${TMP_DIR}/awf" ]; then - echo "ERROR: awf binary not found in archive" +echo "==> Downloading checksums.txt..." +curl -fsSL "$CHECKSUMS_URL" -o "${TMP_DIR}/checksums.txt" + +# Verify SHA256 checksum +echo "==> Verifying checksum..." +EXPECTED_SUM=$(grep " ${ARTIFACT}$" "${TMP_DIR}/checksums.txt" | awk '{print $1}') +if [ -z "$EXPECTED_SUM" ]; then + echo "ERROR: artifact ${ARTIFACT} not found in checksums.txt" exit 1 fi -sudo install -m 0755 "${TMP_DIR}/awf" /usr/local/bin/awf +ACTUAL_SUM=$(sha256sum "${TMP_DIR}/${ARTIFACT}" | awk '{print $1}') +if [ "$EXPECTED_SUM" != "$ACTUAL_SUM" ]; then + echo "ERROR: SHA256 checksum mismatch" + echo " expected: ${EXPECTED_SUM}" + echo " actual: ${ACTUAL_SUM}" + exit 1 +fi +echo "==> Checksum verified" -echo "==> AWF CLI $(awf --version 2>&1) installed at $(command -v awf)" -SCRIPT +# Extract and install binary +tar -xzf "${TMP_DIR}/${ARTIFACT}" -C "$TMP_DIR" + +if [ ! -f "${TMP_DIR}/awf" ]; then + echo "ERROR: awf binary not found in archive" + exit 1 +fi -chmod +x /usr/local/bin/awf-install +install -m 0755 "${TMP_DIR}/awf" /usr/local/bin/awf -echo "==> AWF CLI feature setup complete" -echo " Run 'awf-install' or add it to postCreateCommand to download the binary" +echo "==> AWF CLI $(awf version 2>&1 | head -1) installed at $(command -v awf)" +echo "==> AWF CLI feature install complete" diff --git a/test/awf-cli/install_awf_cli_latest.sh b/test/awf-cli/install_awf_cli_latest.sh index 87d4328..67c303c 100644 --- a/test/awf-cli/install_awf_cli_latest.sh +++ b/test/awf-cli/install_awf_cli_latest.sh @@ -1,24 +1,6 @@ #!/bin/bash set -e -# Scenario: install awf-cli latest (deferred install via awf-install helper) +# Scenario: install latest AWF CLI -echo "==> Testing AWF CLI latest scenario..." - -# awf-install helper must be on PATH -if ! command -v awf-install &>/dev/null; then - echo "FAIL: awf-install not found on PATH" - exit 1 -fi -echo "PASS: awf-install on PATH ($(command -v awf-install))" - -# awf-install without gh auth should fail gracefully -OUTPUT=$(awf-install 2>&1 || true) -if echo "$OUTPUT" | grep -qE "(gh CLI|not authenticated|already installed)"; then - echo "PASS: awf-install fails gracefully without gh auth" -else - echo "FAIL: awf-install unexpected output: ${OUTPUT}" - exit 1 -fi - -echo "==> AWF CLI latest scenario tests passed" +source "$(dirname "$0")/test.sh" diff --git a/test/awf-cli/install_awf_cli_specific_version.sh b/test/awf-cli/install_awf_cli_specific_version.sh new file mode 100644 index 0000000..5241ad5 --- /dev/null +++ b/test/awf-cli/install_awf_cli_specific_version.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +# Scenario: install a specific AWF CLI version (v0.9.0) + +source "$(dirname "$0")/test.sh" + +# Verify the exact version matches +AWF_VERSION=$(awf version 2>&1 | head -1) +if [[ "$AWF_VERSION" != *"0.9.0"* ]]; then + echo "FAIL: expected version 0.9.0, got: ${AWF_VERSION}" + exit 1 +fi +echo "PASS: awf version matches 0.9.0 => ${AWF_VERSION}" + +echo "==> AWF CLI specific version tests passed" diff --git a/test/awf-cli/install_awf_cli_with_gh_auth.sh b/test/awf-cli/install_awf_cli_with_gh_auth.sh deleted file mode 100644 index d2157af..0000000 --- a/test/awf-cli/install_awf_cli_with_gh_auth.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -e - -# Scenario: install awf-cli with gh authenticated -# Requires GH_TOKEN env var with access to awf-project/cli private repo. -# Skips gracefully if no token is available (e.g. in public CI). - -echo "==> Testing AWF CLI with gh auth scenario..." - -# awf-install must exist -if ! command -v awf-install &>/dev/null; then - echo "FAIL: awf-install not found on PATH" - exit 1 -fi -echo "PASS: awf-install on PATH" - -# gh must be available (installed via github-cli feature) -if ! command -v gh &>/dev/null; then - echo "FAIL: gh CLI not found on PATH" - exit 1 -fi -echo "PASS: gh on PATH ($(command -v gh))" - -# Skip actual download if gh is not authenticated -if ! gh auth status &>/dev/null 2>&1; then - echo "SKIP: gh not authenticated — skipping binary download test" - echo "==> AWF CLI with gh auth scenario tests passed (partial)" - exit 0 -fi -echo "PASS: gh is authenticated" - -# Run awf-install to download the binary -awf-install - -# awf binary must be on PATH -if ! command -v awf &>/dev/null; then - echo "FAIL: awf not found on PATH after awf-install" - exit 1 -fi -echo "PASS: awf on PATH ($(command -v awf))" - -# awf --version must succeed -AWF_VERSION=$(awf --version 2>&1) -if [ -z "$AWF_VERSION" ]; then - echo "FAIL: awf --version returned empty output" - exit 1 -fi -echo "PASS: awf --version => ${AWF_VERSION}" - -echo "==> AWF CLI with gh auth scenario tests passed" diff --git a/test/awf-cli/scenarios.json b/test/awf-cli/scenarios.json index 54fcbb8..f9597b9 100644 --- a/test/awf-cli/scenarios.json +++ b/test/awf-cli/scenarios.json @@ -5,11 +5,12 @@ "awf-cli": {} } }, - "install_awf_cli_with_gh_auth": { + "install_awf_cli_specific_version": { "image": "mcr.microsoft.com/devcontainers/base:ubuntu", "features": { - "ghcr.io/devcontainers/features/github-cli:1": {}, - "awf-cli": {} + "awf-cli": { + "version": "v0.9.0" + } } } } diff --git a/test/awf-cli/test.sh b/test/awf-cli/test.sh index bc3d41e..3ee9a32 100644 --- a/test/awf-cli/test.sh +++ b/test/awf-cli/test.sh @@ -1,33 +1,21 @@ #!/bin/bash set -e -# Devcontainer feature test — run by devcontainers/action in CI -# Since awf-cli is a private repo, we can only verify the install helper -# is correctly set up (not the actual binary download, which requires gh auth). - echo "==> Testing AWF CLI feature..." -# awf-install helper must be on PATH -if ! command -v awf-install &>/dev/null; then - echo "FAIL: awf-install not found on PATH" - exit 1 -fi -echo "PASS: awf-install on PATH ($(command -v awf-install))" - -# awf-install must be executable -if [ ! -x "$(command -v awf-install)" ]; then - echo "FAIL: awf-install is not executable" +# awf binary must be on PATH +if ! command -v awf &>/dev/null; then + echo "FAIL: awf not found on PATH" exit 1 fi -echo "PASS: awf-install is executable" +echo "PASS: awf on PATH ($(command -v awf))" -# awf-install without gh auth should fail gracefully (not crash) -OUTPUT=$(awf-install 2>&1 || true) -if echo "$OUTPUT" | grep -qE "(gh CLI|not authenticated|already installed)"; then - echo "PASS: awf-install fails gracefully without gh auth" -else - echo "FAIL: awf-install unexpected output: ${OUTPUT}" +# awf version must succeed and return a version string +AWF_VERSION=$(awf version 2>&1 | head -1) +if [ -z "$AWF_VERSION" ]; then + echo "FAIL: awf version returned empty output" exit 1 fi +echo "PASS: awf version => ${AWF_VERSION}" echo "==> All AWF CLI feature tests passed"