diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 59ceac9..3de8501 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,6 +14,7 @@ jobs: matrix: features: - cato + - rtk baseImage: - debian:latest - ubuntu:latest @@ -34,6 +35,7 @@ jobs: matrix: features: - cato + - rtk steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/update-rtk-sha.yml b/.github/workflows/update-rtk-sha.yml new file mode 100644 index 0000000..06150cb --- /dev/null +++ b/.github/workflows/update-rtk-sha.yml @@ -0,0 +1,86 @@ +name: "Update RTK SHA256 on Renovate PRs" + +on: + pull_request: + branches: + - main + paths: + - "src/rtk/devcontainer-feature.json" + +jobs: + update-sha: + # Only run on Renovate branches — avoids triggering on unrelated PRs that + # happen to touch devcontainer-feature.json (e.g. manual option changes). + if: startsWith(github.head_ref, 'renovate/') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + # Use a token with write access so the subsequent push is accepted. + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read RTK version from feature metadata + id: version + run: | + VERSION=$(jq -r '.options.version.default' src/rtk/devcontainer-feature.json) + echo "rtk_version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "RTK version from feature metadata: ${VERSION}" + + - name: Download RTK tarballs and compute SHA256 + id: sha + run: | + VERSION="${{ steps.version.outputs.rtk_version }}" + TMP=$(mktemp -d) + + AMD64_FILE="rtk-x86_64-unknown-linux-musl.tar.gz" + ARM64_FILE="rtk-aarch64-unknown-linux-gnu.tar.gz" + BASE_URL="https://github.com/rtk-ai/rtk/releases/download/${VERSION}" + + echo "Downloading ${AMD64_FILE}..." + curl -fsSL "${BASE_URL}/${AMD64_FILE}" -o "${TMP}/${AMD64_FILE}" + + echo "Downloading ${ARM64_FILE}..." + curl -fsSL "${BASE_URL}/${ARM64_FILE}" -o "${TMP}/${ARM64_FILE}" + + SHA_AMD64=$(sha256sum "${TMP}/${AMD64_FILE}" | awk '{print $1}') + SHA_ARM64=$(sha256sum "${TMP}/${ARM64_FILE}" | awk '{print $1}') + + echo "sha_amd64=${SHA_AMD64}" >> "$GITHUB_OUTPUT" + echo "sha_arm64=${SHA_ARM64}" >> "$GITHUB_OUTPUT" + echo "amd64 SHA256: ${SHA_AMD64}" + echo "arm64 SHA256: ${SHA_ARM64}" + + rm -rf "${TMP}" + + - name: Patch SHA256 defaults in devcontainer-feature.json + run: | + SHA_AMD64="${{ steps.sha.outputs.sha_amd64 }}" + SHA_ARM64="${{ steps.sha.outputs.sha_arm64 }}" + + jq \ + --arg sha_amd64 "${SHA_AMD64}" \ + --arg sha_arm64 "${SHA_ARM64}" \ + '.options.sha256_amd64.default = $sha_amd64 | .options.sha256_arm64.default = $sha_arm64' \ + src/rtk/devcontainer-feature.json > src/rtk/devcontainer-feature.json.tmp + + mv src/rtk/devcontainer-feature.json.tmp src/rtk/devcontainer-feature.json + echo "Patched devcontainer-feature.json" + cat src/rtk/devcontainer-feature.json + + - name: Commit and push updated SHA256 values + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git diff --quiet; then + echo "No SHA changes — already up to date" + else + git add src/rtk/devcontainer-feature.json + git commit -m "chore(rtk): update SHA256 for ${{ steps.version.outputs.rtk_version }}" + git push + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f5008b..a379504 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,10 +9,16 @@ src/ cato/ devcontainer-feature.json install.sh + rtk/ + devcontainer-feature.json + install.sh test/ cato/ test.sh scenarios.json + rtk/ + test.sh + scenarios.json ``` ## Development diff --git a/README.md b/README.md index 0a64461..0647f75 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,36 @@ Operational expectations: Bundled certificate lifecycle details, validity period, and SHA-256 fingerprint are documented in [src/cato/NOTES.md](src/cato/NOTES.md). +## Feature: rtk + +The `rtk` feature installs the [RTK CLI](https://github.com/rtk-ai/rtk) — a proxy that reduces LLM token consumption by 60–90% on common dev commands. Single Rust binary, zero dependencies. + +Published feature reference: + +```json +"features": { + "ghcr.io/solarwinds/devcontainers/features/rtk:1": {} +} +``` + +### Options + +| Option | Default | Description | +|--------|---------|-------------| +| `version` | `v0.40.0` | RTK release version to install. | +| `sha256_amd64` | *(see feature JSON)* | Expected SHA256 of the amd64 tarball. Leave empty to skip validation (not recommended). | +| `sha256_arm64` | *(see feature JSON)* | Expected SHA256 of the arm64 tarball. Leave empty to skip validation (not recommended). | + +### Keeping RTK Up to Date + +This repository uses [Renovate](https://docs.renovatebot.com/) to track new RTK releases. When a new version of `rtk-ai/rtk` is published on GitHub: + +1. Renovate opens a PR that bumps the `version` default in `src/rtk/devcontainer-feature.json`. +2. The `.github/workflows/update-rtk-sha.yml` workflow triggers automatically on that PR branch, downloads the new release tarballs, computes their SHA256 checksums, and pushes a follow-up commit onto the Renovate PR branch that updates the `sha256_amd64` and `sha256_arm64` defaults. +3. Once CI passes, the PR can be merged as usual. No manual SHA calculation is required. + +**Release**: After merging, bump `version` in `src/rtk/devcontainer-feature.json` and merge to `main` to publish an updated feature package to GHCR. + ## Contributing Development, testing, and release instructions are maintained in [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..cafc84f --- /dev/null +++ b/renovate.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "customManagers": [ + { + "customType": "regex", + "description": "Update RTK version in src/rtk/devcontainer-feature.json. Matches the 'proposals' + 'default' pair in the version option block, which is unique in this file.", + "fileMatch": ["^src/rtk/devcontainer-feature\\.json$"], + "matchStrings": [ + "\"proposals\":\\s*\\[\"(?v[\\d.]+)\"\\],\\s*\"default\":\\s*\"v[\\d.]+\"" + ], + "currentValueTemplate": "{{{currentValue}}}", + "depNameTemplate": "rtk-ai/rtk", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + } + ] +} diff --git a/src/rtk/devcontainer-feature.json b/src/rtk/devcontainer-feature.json new file mode 100644 index 0000000..1a264c3 --- /dev/null +++ b/src/rtk/devcontainer-feature.json @@ -0,0 +1,25 @@ +{ + "id": "rtk", + "version": "1.0.0", + "name": "RTK", + "description": "CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies.", + "documentationURL": "https://github.com/solarwinds/devcontainers/tree/main/src/rtk", + "options": { + "version": { + "type": "string", + "proposals": ["v0.40.0"], + "default": "v0.40.0", + "description": "RTK release version to install (e.g. v0.40.0). renovate: datasource=github-releases depName=rtk-ai/rtk" + }, + "sha256_amd64": { + "type": "string", + "default": "a75d210a445874106bc16da2b4efba01d36d297afa33ec134728f2d5f42ef5af", + "description": "Expected SHA256 of the amd64 tarball for the selected version. Leave empty to skip validation (not recommended)." + }, + "sha256_arm64": { + "type": "string", + "default": "1d0087ad62a182c0833c2251ac678b5e05356418d91aa57305ac51a126c9b102", + "description": "Expected SHA256 of the arm64 tarball for the selected version. Leave empty to skip validation (not recommended)." + } + } +} diff --git a/src/rtk/install.sh b/src/rtk/install.sh new file mode 100755 index 0000000..3494b5f --- /dev/null +++ b/src/rtk/install.sh @@ -0,0 +1,97 @@ +#!/bin/sh +set -eu + +log() { echo "[rtk] $*"; } +die() { echo "[rtk] ERROR: $*" >&2; exit 1; } + +RTK_VERSION="${VERSION:-"v0.40.0"}" +RTK_SHA256_AMD64="${SHA256_AMD64:-}" +RTK_SHA256_ARM64="${SHA256_ARM64:-}" + +log "Activating feature 'rtk'" +log "Version: $RTK_VERSION" + +# ── 1. Detect architecture ──────────────────────────────────────────────────── + +if command -v dpkg >/dev/null 2>&1; then + ARCH="$(dpkg --print-architecture)" +else + case "$(uname -m)" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + *) ARCH="$(uname -m)" ;; + esac +fi + +case "${ARCH}" in + amd64) + RTK_FILE="rtk-x86_64-unknown-linux-musl.tar.gz" + EXPECTED_SHA="${RTK_SHA256_AMD64}" + ;; + arm64) + RTK_FILE="rtk-aarch64-unknown-linux-gnu.tar.gz" + EXPECTED_SHA="${RTK_SHA256_ARM64}" + ;; + *) + die "Unsupported architecture: ${ARCH}" + ;; +esac + +log "Architecture: ${ARCH}" +log "Tarball: ${RTK_FILE}" + +# ── 2. Download ─────────────────────────────────────────────────────────────── + +RTK_URL="https://github.com/rtk-ai/rtk/releases/download/${RTK_VERSION}/${RTK_FILE}" +TMP_DIR="$(mktemp -d)" + +log "Downloading ${RTK_URL}" +curl -fsSL "${RTK_URL}" -o "${TMP_DIR}/${RTK_FILE}" + +# ── 3. SHA256 validation ────────────────────────────────────────────────────── + +if [ -n "${EXPECTED_SHA}" ]; then + log "Validating SHA256..." + echo "${EXPECTED_SHA} ${TMP_DIR}/${RTK_FILE}" | sha256sum -c \ + || { rm -rf "${TMP_DIR}"; die "SHA256 mismatch for ${RTK_FILE}. The download may be corrupt or tampered. Update sha256_amd64/sha256_arm64 options if you changed the version."; } + log "SHA256 OK" +else + log "WARNING: sha256 option is empty — skipping checksum validation. Set sha256_amd64/sha256_arm64 in your feature options to enable it." +fi + +# ── 4. Extract ──────────────────────────────────────────────────────────────── + +log "Extracting to /usr/local/bin..." +tar -xz -f "${TMP_DIR}/${RTK_FILE}" -C /usr/local/bin +chmod +x /usr/local/bin/rtk +rm -rf "${TMP_DIR}" + +# ── 5. Verify binary ────────────────────────────────────────────────────────── + +rtk --version >/dev/null 2>&1 || die "rtk binary not functional after install" +log "rtk installed: $(rtk --version 2>&1)" + +# ── 6. Post-install configuration as the target user ───────────────────────── + +TARGET_USER="${_REMOTE_USER:-}" + +run_as_user() { + if [ -z "${TARGET_USER}" ] || [ "${TARGET_USER}" = "root" ] || [ "$(id -un)" = "${TARGET_USER}" ]; then + "$@" + elif command -v runuser >/dev/null 2>&1; then + runuser -l "${TARGET_USER}" -c "$*" + elif command -v su >/dev/null 2>&1; then + su - "${TARGET_USER}" -c "$*" + else + log "WARNING: cannot switch to user '${TARGET_USER}'; running as $(id -un)" + "$@" + fi +} + +log "Disabling RTK telemetry..." +run_as_user rtk telemetry disable + +log "Running rtk init..." +run_as_user rtk init -g --auto-patch + +log "Feature setup finished" diff --git a/test/rtk/_scenario_assertions.sh b/test/rtk/_scenario_assertions.sh new file mode 100755 index 0000000..1784398 --- /dev/null +++ b/test/rtk/_scenario_assertions.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu + +echo "[rtk-scenario] Verifying rtk installation" + +command -v rtk +rtk --version + +echo "[rtk-scenario] OK" diff --git a/test/rtk/base_jammy.sh b/test/rtk/base_jammy.sh new file mode 100755 index 0000000..f82c1db --- /dev/null +++ b/test/rtk/base_jammy.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +"$(dirname "$0")/_scenario_assertions.sh" diff --git a/test/rtk/base_ubuntu.sh b/test/rtk/base_ubuntu.sh new file mode 100755 index 0000000..f82c1db --- /dev/null +++ b/test/rtk/base_ubuntu.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +"$(dirname "$0")/_scenario_assertions.sh" diff --git a/test/rtk/scenarios.json b/test/rtk/scenarios.json new file mode 100644 index 0000000..14de4dc --- /dev/null +++ b/test/rtk/scenarios.json @@ -0,0 +1,16 @@ +{ + "base_jammy": { + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "rtk": {}, + "ghcr.io/devcontainers/features/common-utils:2": {} + } + }, + "base_ubuntu": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "rtk": {}, + "ghcr.io/devcontainers/features/common-utils:2": {} + } + } +} diff --git a/test/rtk/test.sh b/test/rtk/test.sh new file mode 100755 index 0000000..e2dea4f --- /dev/null +++ b/test/rtk/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "rtk installed" bash -c "command -v rtk" +check "rtk version runs" rtk --version + +reportResults