Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
17 changes: 12 additions & 5 deletions src/awf-cli/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
116 changes: 61 additions & 55 deletions src/awf-cli/install.sh
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 2 additions & 20 deletions test/awf-cli/install_awf_cli_latest.sh
Original file line number Diff line number Diff line change
@@ -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"
16 changes: 16 additions & 0 deletions test/awf-cli/install_awf_cli_specific_version.sh
Original file line number Diff line number Diff line change
@@ -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"
50 changes: 0 additions & 50 deletions test/awf-cli/install_awf_cli_with_gh_auth.sh

This file was deleted.

7 changes: 4 additions & 3 deletions test/awf-cli/scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
30 changes: 9 additions & 21 deletions test/awf-cli/test.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading