diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 518e2ef..30ca261 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -19,6 +19,7 @@ jobs: - copilot-persistence - gitlab-ci-local - atlassian-jira-confluence-cli + - atlassian-teamwork-graph-cli - package-source-overrides baseImage: - debian:latest @@ -45,6 +46,7 @@ jobs: - copilot-persistence - gitlab-ci-local - atlassian-jira-confluence-cli + - atlassian-teamwork-graph-cli - package-source-overrides steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index f123840..f742d5b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Custom [dev container Features](https://containers.dev/implementors/features/) f | [Microsoft Security DevOps CLI](src/microsoft-security-devops-cli) | `guardian` CLI for running security analysis tools without .NET | | [Copilot CLI Persistence](src/copilot-persistence) | Persist GitHub Copilot CLI settings & chat history across rebuilds | | [Atlassian CLI for Jira & Confluence](src/atlassian-jira-confluence-cli) | `acli` for working with Jira and Confluence Cloud | +| [Atlassian Teamwork Graph CLI](src/atlassian-teamwork-graph-cli) | `twg` for Jira, Confluence, Bitbucket, JSM and the Teamwork Graph, built for AI agents | | [Package Source Overrides](src/package-source-overrides) | Point npm, pnpm, Yarn, Corepack, pip, uv, and NuGet at internal override feeds | | [GitLab CI Local](src/gitlab-ci-local) | Run GitLab CI/CD pipelines locally with `gitlab-ci-local` | | [Prompty Dumpty](src/prompty-dumpty) | `dumpty` CLI for managing prompty files | @@ -156,6 +157,39 @@ acli jira project list `acli auth login` opens an OAuth page via `xdg-open`, which doesn't honor `$BROWSER` inside a dev container. With `configureBrowser` enabled (the default), the feature installs a small `xdg-open` shim at `/usr/local/bin/xdg-open` that prefers `$BROWSER` (such as the VS Code browser helper) so the login page opens on your host machine. Set `configureBrowser` to `false` to skip it. +### Atlassian Teamwork Graph CLI + +Installs the [Atlassian Teamwork Graph CLI](https://developer.atlassian.com/platform/teamwork-graph/twg-cli/) (`twg`) for working with Jira, Confluence, JSM, Bitbucket, Loom, Trello and the Teamwork Graph, from the terminal or from AI coding agents. It complements `acli`, and the two Features can be used together. + +**Usage:** + +```json +{ + "features": { + "ghcr.io/rosstaco/devcontainer-features/atlassian-teamwork-graph-cli:1": {} + }, + "remoteEnv": { + "TWG_TOKEN": "${localEnv:TWG_TOKEN}", + "TWG_USER": "${localEnv:TWG_USER}", + "TWG_SITE": "${localEnv:TWG_SITE}" + } +} +``` + +The `remoteEnv` block is optional. Use it to pass credentials from your host instead of running `twg login`. + +**Options:** +- `version` - Version of twg to install (default: "latest"). Use "latest" or a specific version like "1.3.5" +- `installSkills` - Install the TWG agent skills into `~/.agents/skills` and `~/.claude/skills` (default: `false`) +- `enableTelemetry` - Allow twg telemetry. When `false`, `DO_NOT_TRACK=1` and `TWG_DISABLE_3P_DATA_TELEMETRY=1` are exported (default: `false`) +- `persistAuth` - Keep `~/.config/twg` on a named volume so `twg login` survives rebuilds (default: `true`) +- `configureBrowser` - Install an `xdg-open` shim that prefers `$BROWSER` so `twg login` opens on your host (default: `true`) + +**How It Works:** +- Downloads the official binary from Atlassian and checks its SHA-256 checksum. It skips the interactive installer, which asks for consent and login during the build. +- Installs to `~/.local/bin/twg` for the remote user, with a `/usr/local/bin/twg` symlink +- Run `twg login` and then `twg doctor` once the container starts + ### Package Source Overrides Points npm, pnpm, Yarn, Corepack, pip, uv, and NuGet at internal **override package sources** (a pull-through proxy such as Artifactory, Nexus, or Azure Artifacts) instead of public feeds. It writes system- and user-scoped configuration so that Features installed after it, and your projects, resolve packages through the proxy. diff --git a/justfile b/justfile index ebcd0d3..c4490b8 100644 --- a/justfile +++ b/justfile @@ -25,6 +25,10 @@ build-gitlab-ci-local: build-atlassian-jira-confluence-cli: just build-feature atlassian-jira-confluence-cli +# Build Atlassian Teamwork Graph CLI (twg) feature +build-atlassian-teamwork-graph-cli: + just build-feature atlassian-teamwork-graph-cli + # Build Package Source Overrides feature build-package-source-overrides: just build-feature package-source-overrides @@ -35,6 +39,7 @@ build-all: just build-microsoft-security-devops-cli just build-gitlab-ci-local just build-atlassian-jira-confluence-cli + just build-atlassian-teamwork-graph-cli just build-package-source-overrides # Generate feature README.md files from devcontainer-feature.json and NOTES.md @@ -46,6 +51,7 @@ clean: rm -rf .devcontainer/ohmyposh rm -rf .devcontainer/gitlab-ci-local rm -rf .devcontainer/atlassian-jira-confluence-cli + rm -rf .devcontainer/atlassian-teamwork-graph-cli rm -rf .devcontainer/package-source-overrides rm -f .devcontainer/*.tgz echo "✓ Cleaned local features" diff --git a/src/atlassian-teamwork-graph-cli/NOTES.md b/src/atlassian-teamwork-graph-cli/NOTES.md new file mode 100644 index 0000000..e020b4d --- /dev/null +++ b/src/atlassian-teamwork-graph-cli/NOTES.md @@ -0,0 +1,79 @@ +## About + +The [Atlassian Teamwork Graph CLI](https://developer.atlassian.com/platform/teamwork-graph/twg-cli/) (`twg`) is Atlassian's CLI for the Teamwork Graph and Atlassian Cloud: Jira, Confluence, Jira Service Management, Bitbucket, Loom, Trello, Goals, Assets and more. It is designed to be driven by AI coding agents as well as people, and complements the [Atlassian CLI (`acli`)](https://github.com/rosstaco/devcontainer-features/tree/main/src/atlassian-jira-confluence-cli). Both Features can be used together. + +## How It Works + +Atlassian's official installer (`curl … /cli/install | bash`) also runs `twg setup`, which asks you to accept the Atlassian agreements, installs agent skills and starts an interactive OAuth login on the controlling terminal. None of that can happen while an image is being built, so this Feature: + +1. **Downloads the official binary** from `teamwork-graph.atlassian.com/cli` (`latest` is resolved from the stable `manifest.json`) and **verifies its SHA-256** against the published `SHA256SUMS` +2. **Installs it for the remote user** at `~/.local/bin/twg` (twg's default location, user-owned so `twg upgrade` works without sudo) and symlinks `/usr/local/bin/twg` to it so it is on every `PATH` +3. **Disables telemetry** unless `enableTelemetry` is `true` (see below) +4. **Persists `~/.config/twg`** on a per-dev-container named volume when `persistAuth` is `true`, so you only log in once +5. **Optionally installs TWG agent skills** (`installSkills`) +6. **Installs an `xdg-open` shim** (when `configureBrowser` is `true`) so the OAuth login opens in your host browser + +Supported architectures: `x86_64` and `arm64` (Linux, glibc). + +## Getting Started + +Authenticate once, then check the setup: + +```bash +twg login # OAuth device authorization: open the printed URL (or let $BROWSER open it) and approve +twg doctor +``` + +Then, for example: + +```bash +twg jira --help +twg confluence --help +twg --output json jira ... +``` + +## Authenticating with Environment Variables + +Instead of `twg login`, every `twg` command reads `TWG_TOKEN`, `TWG_USER` and `TWG_SITE` directly. A Feature can't safely set these (its `containerEnv` is baked into the image), so pass them from your host in your own `devcontainer.json` with `remoteEnv`: + +```json +{ + "features": { + "ghcr.io/rosstaco/devcontainer-features/atlassian-teamwork-graph-cli:1": {} + }, + "remoteEnv": { + "TWG_TOKEN": "${localEnv:TWG_TOKEN}", + "TWG_USER": "${localEnv:TWG_USER}", + "TWG_SITE": "${localEnv:TWG_SITE}" + } +} +``` + +`remoteEnv` values are applied to the processes VS Code (or the dev container CLI) starts and are not written to the image. Never commit a token to `devcontainer.json`. + +## Telemetry + +With `enableTelemetry` set to `false` (the default), the Feature writes `/etc/atlassian-teamwork-graph-cli/env.sh`: + +```bash +export DO_NOT_TRACK=1 +export TWG_DISABLE_3P_DATA_TELEMETRY=1 +``` + +It is sourced by login shells via `/etc/profile.d/atlassian-teamwork-graph-cli.sh` and by non-interactive bash via `BASH_ENV` (`/etc/atlassian-teamwork-graph-cli/bash_env`, which chains to any `BASH_ENV` set before this Feature). `DO_NOT_TRACK` is the [Console Do Not Track](https://consoledonottrack.com/) convention, so other tools that honour it will also stop sending telemetry. Set `enableTelemetry` to `true` to skip this. + +## Persisted Login + +`twg` keeps its credentials and settings in `~/.config/twg`. The Feature always mounts a named volume at `/twg-config` (dev container mounts can't depend on options). With `persistAuth` set to `true` (the default), `~/.config/twg` is a symlink to that volume, so `twg login` survives rebuilds. Set `persistAuth` to `false` to keep the config in the container instead. To log out, run `twg logout`, or remove the `-twg-config` volume. + +## Agent Skills + +With `installSkills` set to `true`, the Feature runs `twg skills install --yes` as the remote user, installing the TWG skills into `~/.agents/skills` (used by Copilot, Codex, Cursor, Gemini and other `.agents/skills`-compatible agents) and `~/.claude/skills`. They are reinstalled on every rebuild. You can also run `twg skills install` yourself. + +## Browser Login in a Dev Container + +`twg login` tries to open the OAuth page with `xdg-open`, which doesn't honour `$BROWSER` inside a dev container. With `configureBrowser` enabled, this Feature installs a small `xdg-open` shim at `/usr/local/bin/xdg-open` that prefers `$BROWSER` (for example the VS Code browser helper) and falls back to the real `xdg-open` otherwise. `twg login` also prints the URL, so you can open it yourself. The shim is the same one the `atlassian-jira-confluence-cli` Feature installs. + +## Versions and Upgrades + +`latest` installs the current stable release at build time. Atlassian publishes a minimum supported version and blocks older builds, so a pinned `version` may stop working over time. `twg upgrade` works inside the container, but the image installs the configured version again on the next rebuild. diff --git a/src/atlassian-teamwork-graph-cli/README.md b/src/atlassian-teamwork-graph-cli/README.md new file mode 100644 index 0000000..18db76e --- /dev/null +++ b/src/atlassian-teamwork-graph-cli/README.md @@ -0,0 +1,107 @@ + +# Atlassian Teamwork Graph CLI (twg) (atlassian-teamwork-graph-cli) + +Installs the Atlassian Teamwork Graph CLI (twg) for working with Jira, Confluence, Bitbucket, JSM, Loom, Trello and the Teamwork Graph from the command line and from AI coding agents + +## Example Usage + +```json +"features": { + "ghcr.io/rosstaco/devcontainer-features/atlassian-teamwork-graph-cli:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Version of twg to install. Use 'latest' (current stable release) or a specific version like '1.3.5'. Atlassian blocks versions below a published minimum, so old pins may stop working | string | latest | +| installSkills | Run 'twg skills install --yes' for the remote user, installing TWG agent skills into ~/.agents/skills and ~/.claude/skills | boolean | false | +| enableTelemetry | Allow twg runtime telemetry. When false (the default), DO_NOT_TRACK=1 and TWG_DISABLE_3P_DATA_TELEMETRY=1 are exported for login shells (/etc/profile.d) and non-interactive bash (BASH_ENV) | boolean | false | +| persistAuth | Symlink ~/.config/twg (credentials, site, settings) to a per-dev-container named volume so 'twg login' survives container rebuilds | boolean | true | +| configureBrowser | Install an xdg-open shim in /usr/local/bin that prefers $BROWSER (e.g. the VS Code browser helper) so 'twg login' opens the OAuth page on your host instead of failing inside the container | boolean | true | + +## About + +The [Atlassian Teamwork Graph CLI](https://developer.atlassian.com/platform/teamwork-graph/twg-cli/) (`twg`) is Atlassian's CLI for the Teamwork Graph and Atlassian Cloud: Jira, Confluence, Jira Service Management, Bitbucket, Loom, Trello, Goals, Assets and more. It is designed to be driven by AI coding agents as well as people, and complements the [Atlassian CLI (`acli`)](https://github.com/rosstaco/devcontainer-features/tree/main/src/atlassian-jira-confluence-cli). Both Features can be used together. + +## How It Works + +Atlassian's official installer (`curl … /cli/install | bash`) also runs `twg setup`, which asks you to accept the Atlassian agreements, installs agent skills and starts an interactive OAuth login on the controlling terminal. None of that can happen while an image is being built, so this Feature: + +1. **Downloads the official binary** from `teamwork-graph.atlassian.com/cli` (`latest` is resolved from the stable `manifest.json`) and **verifies its SHA-256** against the published `SHA256SUMS` +2. **Installs it for the remote user** at `~/.local/bin/twg` (twg's default location, user-owned so `twg upgrade` works without sudo) and symlinks `/usr/local/bin/twg` to it so it is on every `PATH` +3. **Disables telemetry** unless `enableTelemetry` is `true` (see below) +4. **Persists `~/.config/twg`** on a per-dev-container named volume when `persistAuth` is `true`, so you only log in once +5. **Optionally installs TWG agent skills** (`installSkills`) +6. **Installs an `xdg-open` shim** (when `configureBrowser` is `true`) so the OAuth login opens in your host browser + +Supported architectures: `x86_64` and `arm64` (Linux, glibc). + +## Getting Started + +Authenticate once, then check the setup: + +```bash +twg login # OAuth device authorization: open the printed URL (or let $BROWSER open it) and approve +twg doctor +``` + +Then, for example: + +```bash +twg jira --help +twg confluence --help +twg --output json jira ... +``` + +## Authenticating with Environment Variables + +Instead of `twg login`, every `twg` command reads `TWG_TOKEN`, `TWG_USER` and `TWG_SITE` directly. A Feature can't safely set these (its `containerEnv` is baked into the image), so pass them from your host in your own `devcontainer.json` with `remoteEnv`: + +```json +{ + "features": { + "ghcr.io/rosstaco/devcontainer-features/atlassian-teamwork-graph-cli:1": {} + }, + "remoteEnv": { + "TWG_TOKEN": "${localEnv:TWG_TOKEN}", + "TWG_USER": "${localEnv:TWG_USER}", + "TWG_SITE": "${localEnv:TWG_SITE}" + } +} +``` + +`remoteEnv` values are applied to the processes VS Code (or the dev container CLI) starts and are not written to the image. Never commit a token to `devcontainer.json`. + +## Telemetry + +With `enableTelemetry` set to `false` (the default), the Feature writes `/etc/atlassian-teamwork-graph-cli/env.sh`: + +```bash +export DO_NOT_TRACK=1 +export TWG_DISABLE_3P_DATA_TELEMETRY=1 +``` + +It is sourced by login shells via `/etc/profile.d/atlassian-teamwork-graph-cli.sh` and by non-interactive bash via `BASH_ENV` (`/etc/atlassian-teamwork-graph-cli/bash_env`, which chains to any `BASH_ENV` set before this Feature). `DO_NOT_TRACK` is the [Console Do Not Track](https://consoledonottrack.com/) convention, so other tools that honour it will also stop sending telemetry. Set `enableTelemetry` to `true` to skip this. + +## Persisted Login + +`twg` keeps its credentials and settings in `~/.config/twg`. The Feature always mounts a named volume at `/twg-config` (dev container mounts can't depend on options). With `persistAuth` set to `true` (the default), `~/.config/twg` is a symlink to that volume, so `twg login` survives rebuilds. Set `persistAuth` to `false` to keep the config in the container instead. To log out, run `twg logout`, or remove the `-twg-config` volume. + +## Agent Skills + +With `installSkills` set to `true`, the Feature runs `twg skills install --yes` as the remote user, installing the TWG skills into `~/.agents/skills` (used by Copilot, Codex, Cursor, Gemini and other `.agents/skills`-compatible agents) and `~/.claude/skills`. They are reinstalled on every rebuild. You can also run `twg skills install` yourself. + +## Browser Login in a Dev Container + +`twg login` tries to open the OAuth page with `xdg-open`, which doesn't honour `$BROWSER` inside a dev container. With `configureBrowser` enabled, this Feature installs a small `xdg-open` shim at `/usr/local/bin/xdg-open` that prefers `$BROWSER` (for example the VS Code browser helper) and falls back to the real `xdg-open` otherwise. `twg login` also prints the URL, so you can open it yourself. The shim is the same one the `atlassian-jira-confluence-cli` Feature installs. + +## Versions and Upgrades + +`latest` installs the current stable release at build time. Atlassian publishes a minimum supported version and blocks older builds, so a pinned `version` may stop working over time. `twg upgrade` works inside the container, but the image installs the configured version again on the next rebuild. + + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/rosstaco/devcontainer-features/blob/main/src/atlassian-teamwork-graph-cli/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/atlassian-teamwork-graph-cli/devcontainer-feature.json b/src/atlassian-teamwork-graph-cli/devcontainer-feature.json new file mode 100644 index 0000000..e903869 --- /dev/null +++ b/src/atlassian-teamwork-graph-cli/devcontainer-feature.json @@ -0,0 +1,62 @@ +{ + "id": "atlassian-teamwork-graph-cli", + "version": "1.0.0", + "name": "Atlassian Teamwork Graph CLI (twg)", + "description": "Installs the Atlassian Teamwork Graph CLI (twg) for working with Jira, Confluence, Bitbucket, JSM, Loom, Trello and the Teamwork Graph from the command line and from AI coding agents", + "documentationURL": "https://github.com/rosstaco/devcontainer-features/tree/main/src/atlassian-teamwork-graph-cli", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest" + ], + "default": "latest", + "description": "Version of twg to install. Use 'latest' (current stable release) or a specific version like '1.3.5'. Atlassian blocks versions below a published minimum, so old pins may stop working" + }, + "installSkills": { + "type": "boolean", + "default": false, + "description": "Run 'twg skills install --yes' for the remote user, installing TWG agent skills into ~/.agents/skills and ~/.claude/skills" + }, + "enableTelemetry": { + "type": "boolean", + "default": false, + "description": "Allow twg runtime telemetry. When false (the default), DO_NOT_TRACK=1 and TWG_DISABLE_3P_DATA_TELEMETRY=1 are exported for login shells (/etc/profile.d) and non-interactive bash (BASH_ENV)" + }, + "persistAuth": { + "type": "boolean", + "default": true, + "description": "Symlink ~/.config/twg (credentials, site, settings) to a per-dev-container named volume so 'twg login' survives container rebuilds" + }, + "configureBrowser": { + "type": "boolean", + "default": true, + "description": "Install an xdg-open shim in /usr/local/bin that prefers $BROWSER (e.g. the VS Code browser helper) so 'twg login' opens the OAuth page on your host instead of failing inside the container" + } + }, + "mounts": [ + { + "source": "${devcontainerId}-twg-config", + "target": "/twg-config", + "type": "volume" + } + ], + "containerEnv": { + "ATLASSIAN_TEAMWORK_GRAPH_CLI_PREV_BASH_ENV": "${BASH_ENV}", + "BASH_ENV": "/etc/atlassian-teamwork-graph-cli/bash_env" + }, + "customizations": { + "vscode": { + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes `twg`, the Atlassian Teamwork Graph CLI, pre-installed and available on the `PATH`. Use it to work with Jira, Confluence, Bitbucket, JSM and other Atlassian Cloud products from the command line (run `twg --help` to discover commands, and prefer `--output json` for machine-readable output). Authenticate with `twg login` (interactive) or the `TWG_TOKEN`/`TWG_USER`/`TWG_SITE` environment variables; never ask for, echo or log a token." + } + ] + } + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/src/atlassian-teamwork-graph-cli/install.sh b/src/atlassian-teamwork-graph-cli/install.sh new file mode 100644 index 0000000..58a6a89 --- /dev/null +++ b/src/atlassian-teamwork-graph-cli/install.sh @@ -0,0 +1,275 @@ +#!/bin/bash +set -e + +# atlassian-teamwork-graph-cli +# ---------------------------- +# Installs the Atlassian Teamwork Graph CLI (twg) for the remote user. +# https://developer.atlassian.com/platform/teamwork-graph/twg-cli/ +# +# The official installer (https://teamwork-graph.atlassian.com/cli/install) also +# runs 'twg setup finalize', which records consent, installs skills and starts an +# interactive OAuth login on /dev/tty. That can't happen at image build time, so +# this downloads the binary directly from the same CDN and verifies it against +# the published SHA256SUMS. Consent and login are left to the user ('twg login'). + +TWG_VERSION="${VERSION:-latest}" +INSTALL_SKILLS="${INSTALLSKILLS:-false}" +ENABLE_TELEMETRY="${ENABLETELEMETRY:-false}" +PERSIST_AUTH="${PERSISTAUTH:-true}" +CONFIGURE_BROWSER="${CONFIGUREBROWSER:-true}" + +CDN_BASE_URL="https://teamwork-graph.atlassian.com/cli" +# Fixed paths; BASH_ENV_FILE and CONFIG_VOLUME are also referenced by +# devcontainer-feature.json (containerEnv BASH_ENV and mounts). +STATE_DIR="/etc/atlassian-teamwork-graph-cli" +ENV_FILE="${STATE_DIR}/env.sh" +BASH_ENV_FILE="${STATE_DIR}/bash_env" +PROFILE_FILE="/etc/profile.d/atlassian-teamwork-graph-cli.sh" +CONFIG_VOLUME="/twg-config" +SYMLINK_PATH="/usr/local/bin/twg" +# The BASH_ENV value from before this Feature's containerEnv replaced it. +PREV_BASH_ENV="${ATLASSIAN_TEAMWORK_GRAPH_CLI_PREV_BASH_ENV:-}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +log() { echo -e "${GREEN}[atlassian-teamwork-graph-cli]${NC} $*"; } +warn() { echo -e "${YELLOW}[atlassian-teamwork-graph-cli]${NC} $*" >&2; } +err() { echo -e "${RED}[atlassian-teamwork-graph-cli]${NC} $*" >&2; } + +if [ "$(id -u)" -ne 0 ]; then + err "Script must be run as root. Add \"USER root\" before running, or use sudo." + exit 1 +fi + +# --- Resolve remote user + home --------------------------------------------- +REMOTE_USER="${_REMOTE_USER:-"${USERNAME:-vscode}"}" +if [ "${REMOTE_USER}" = "none" ] || ! id -u "${REMOTE_USER}" >/dev/null 2>&1; then + REMOTE_USER="root" +fi +if [ "${REMOTE_USER}" = "${_REMOTE_USER:-}" ] && [ -n "${_REMOTE_USER_HOME:-}" ]; then + USER_HOME="${_REMOTE_USER_HOME}" +else + USER_HOME="$(getent passwd "${REMOTE_USER}" 2>/dev/null | cut -d: -f6)" +fi +if [ -z "${USER_HOME}" ]; then + err "Could not determine home directory for user '${REMOTE_USER}'" + exit 1 +fi +USER_UID="$(id -u "${REMOTE_USER}")" +USER_GID="$(id -g "${REMOTE_USER}")" +TWG_BIN="${USER_HOME}/.local/bin/twg" + +# Create a directory owned by the remote user, leaving existing ones untouched. +ensure_user_dir() { + if [ ! -d "$1" ]; then + mkdir -p "$1" + chown "${USER_UID}:${USER_GID}" "$1" + fi +} + + +# --- Dependencies ----------------------------------------------------------- +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} + +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update + apt-get -y install --no-install-recommends "$@" + fi +} + +# --- Telemetry environment -------------------------------------------------- +# The variables depend on an option, which static containerEnv cannot express. +# They are written to ENV_FILE and exported for login shells via /etc/profile.d +# and for non-interactive bash via BASH_ENV (chained to any previous BASH_ENV). +configure_env() { + mkdir -p "${STATE_DIR}" + if [ "${ENABLE_TELEMETRY}" = "true" ]; then + rm -f "${ENV_FILE}" + return 0 + fi + log "Disabling twg telemetry (DO_NOT_TRACK=1, TWG_DISABLE_3P_DATA_TELEMETRY=1)" + cat > "${ENV_FILE}" <<'EOF' +# Managed by the atlassian-teamwork-graph-cli dev container Feature +export DO_NOT_TRACK=1 +export TWG_DISABLE_3P_DATA_TELEMETRY=1 +EOF + # Also applies to the twg commands run later in this script. + . "${ENV_FILE}" +} + +configure_bash_env() { + local prev="" content + if [ -n "${PREV_BASH_ENV}" ] && [ "${PREV_BASH_ENV}" != "${BASH_ENV_FILE}" ]; then + prev="${PREV_BASH_ENV}" + fi + if [ ! -f "${ENV_FILE}" ] && [ -z "${prev}" ]; then + rm -f "${BASH_ENV_FILE}" + return 0 + fi + content="# Managed by the atlassian-teamwork-graph-cli dev container Feature (sourced via BASH_ENV)" + if [ -n "${prev}" ]; then + content="${content} +# Chain to the BASH_ENV that was set before this Feature +if [ -r '${prev}' ]; then . '${prev}'; fi" + fi + if [ -f "${ENV_FILE}" ]; then + content="${content} +if [ -r ${ENV_FILE} ]; then . ${ENV_FILE}; fi" + fi + printf '%s\n' "${content}" > "${BASH_ENV_FILE}" +} + +# --- Persistent auth -------------------------------------------------------- +# ~/.config/twg holds auth.conf (credentials/site) and settings. Pointing it at +# the named volume declared in devcontainer-feature.json keeps 'twg login' +# across rebuilds. The volume is always mounted (mounts can't be conditional); +# persistAuth only controls the symlink. +configure_persist_auth() { + mkdir -p "${CONFIG_VOLUME}" + chown "${USER_UID}:${USER_GID}" "${CONFIG_VOLUME}" + chmod 700 "${CONFIG_VOLUME}" + if [ "${PERSIST_AUTH}" != "true" ]; then + return 0 + fi + log "Persisting ${USER_HOME}/.config/twg -> ${CONFIG_VOLUME}" + ensure_user_dir "${USER_HOME}/.config" + if [ -e "${USER_HOME}/.config/twg" ] && [ ! -L "${USER_HOME}/.config/twg" ]; then + mv "${USER_HOME}/.config/twg" "${CONFIG_VOLUME}/migrated-$(date +%s%N)-$$" + fi + ln -sfn "${CONFIG_VOLUME}" "${USER_HOME}/.config/twg" + chown -h "${USER_UID}:${USER_GID}" "${USER_HOME}/.config/twg" +} + +write_profile() { + local content="" + if [ -f "${ENV_FILE}" ]; then + content="${content} +if [ -r ${ENV_FILE} ]; then . ${ENV_FILE}; fi" + fi + if [ "${PERSIST_AUTH}" = "true" ]; then + # Build-time ownership may not carry into a mounted volume. + content="${content} +[ -d ${CONFIG_VOLUME} ] && [ ! -w ${CONFIG_VOLUME} ] && sudo -n chown -R \"\$(id -u):\$(id -g)\" ${CONFIG_VOLUME} 2>/dev/null || true" + fi + if [ -z "${content}" ]; then + rm -f "${PROFILE_FILE}" + return 0 + fi + printf '%s\n' "# Managed by the atlassian-teamwork-graph-cli dev container Feature${content}" > "${PROFILE_FILE}" +} + +# --- Install binary --------------------------------------------------------- +install_twg() { + local arch version tmp_dir binary_name expected_sha actual_sha + + case "$(uname -m)" in + x86_64|amd64) arch="x64" ;; + aarch64|arm64) arch="arm64" ;; + *) + err "Unsupported architecture: $(uname -m). Only x86_64 and arm64 are supported." + exit 1 + ;; + esac + + if [ "${TWG_VERSION}" = "latest" ]; then + log "Resolving latest stable twg version from ${CDN_BASE_URL}/manifest.json" + version="$(curl -fsSL --retry 2 "${CDN_BASE_URL}/manifest.json" \ + | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 \ + | sed 's/.*"\([^"]*\)"$/\1/')" + if [ -z "${version}" ]; then + err "Could not determine the latest twg version from ${CDN_BASE_URL}/manifest.json" + exit 1 + fi + else + version="${TWG_VERSION#v}" + fi + if ! printf '%s' "${version}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.+-][0-9A-Za-z.-]+)?$'; then + err "Invalid twg version '${version}'. Use 'latest' or a version like '1.3.5'." + exit 1 + fi + + binary_name="twg-linux-${arch}-v${version}" + tmp_dir="$(mktemp -d)" + # shellcheck disable=SC2064 + trap "rm -rf '${tmp_dir}'" EXIT + + log "Downloading ${CDN_BASE_URL}/${binary_name}" + if ! curl -fsSL --retry 2 -o "${tmp_dir}/twg" "${CDN_BASE_URL}/${binary_name}"; then + err "Failed to download twg ${version}. Check that the version exists." + exit 1 + fi + curl -fsSL --retry 2 -o "${tmp_dir}/SHA256SUMS" "${CDN_BASE_URL}/SHA256SUMS-v${version}" + + expected_sha="$(awk -v file="${binary_name}" '{ name = $2; sub(/^\*/, "", name); sub(/\r$/, "", name); if (name == file) { print $1; exit } }' "${tmp_dir}/SHA256SUMS")" + actual_sha="$(sha256sum "${tmp_dir}/twg" | awk '{print $1}')" + if [ -z "${expected_sha}" ] || [ "${expected_sha}" != "${actual_sha}" ]; then + err "Checksum verification failed for ${binary_name} (expected '${expected_sha}', got '${actual_sha}')" + exit 1 + fi + + # Installed user-owned in ~/.local/bin (twg's default launcher path) so + # 'twg upgrade' can replace it without sudo; symlinked onto the system PATH. + ensure_user_dir "${USER_HOME}/.local" + ensure_user_dir "${USER_HOME}/.local/bin" + install -m 0755 -o "${USER_UID}" -g "${USER_GID}" "${tmp_dir}/twg" "${TWG_BIN}" + ln -sfn "${TWG_BIN}" "${SYMLINK_PATH}" + + INSTALLED_VERSION="$("${TWG_BIN}" -v 2>/dev/null | head -n 1 || true)" + if [ -z "${INSTALLED_VERSION}" ]; then + err "twg was installed to ${TWG_BIN} but failed to run" + exit 1 + fi + log "twg ${INSTALLED_VERSION} installed to ${TWG_BIN} (symlinked from ${SYMLINK_PATH})" +} + +install_skills() { + if [ "${INSTALL_SKILLS}" != "true" ]; then + return 0 + fi + log "Installing TWG agent skills for ${REMOTE_USER}" + # su (without -l) keeps the telemetry variables and sets HOME for the user. + if [ "${REMOTE_USER}" = "root" ]; then + HOME="${USER_HOME}" "${TWG_BIN}" skills install --yes + else + su "${REMOTE_USER}" -s /bin/sh -c "'${TWG_BIN}' skills install --yes" + fi +} + +# twg login opens the OAuth page via xdg-open, which doesn't honor $BROWSER +# inside a dev container. The shim prefers $BROWSER (e.g. VS Code's helper). +configure_browser() { + if [ "${CONFIGURE_BROWSER}" != "true" ]; then + return 0 + fi + log "Installing xdg-open shim that prefers \$BROWSER" + cat > /usr/local/bin/xdg-open <<'SHIM' +#!/bin/sh +[ -n "$BROWSER" ] && [ -x "$BROWSER" ] && exec "$BROWSER" "$@" +exec /usr/bin/xdg-open "$@" +SHIM + chmod 0755 /usr/local/bin/xdg-open +} + +# --- Main ------------------------------------------------------------------- +check_packages curl ca-certificates coreutils +configure_env +configure_bash_env +configure_persist_auth +write_profile +install_twg +install_skills +configure_browser + +log "Done. remoteUser=${REMOTE_USER} (${USER_HOME}) telemetry=${ENABLE_TELEMETRY} persistAuth=${PERSIST_AUTH} skills=${INSTALL_SKILLS}" +echo "" +echo "Run 'twg login' to authenticate (or set TWG_TOKEN/TWG_USER/TWG_SITE), then 'twg doctor'." +echo "For more information: https://developer.atlassian.com/platform/teamwork-graph/twg-cli/" diff --git a/test/atlassian-teamwork-graph-cli/opt-outs.sh b/test/atlassian-teamwork-graph-cli/opt-outs.sh new file mode 100644 index 0000000..6fbbb7d --- /dev/null +++ b/test/atlassian-teamwork-graph-cli/opt-outs.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Scenario 'opt-outs': enableTelemetry=true, persistAuth=false, configureBrowser=false. + +set -e + +source dev-container-features-test-lib + +check "twg is on PATH" command -v twg + +check "no telemetry env file" bash -c '[ ! -e /etc/atlassian-teamwork-graph-cli/env.sh ]' + +check "no BASH_ENV hook file" bash -c '[ ! -e /etc/atlassian-teamwork-graph-cli/bash_env ]' + +check "login shells do not get DO_NOT_TRACK" bash -c '[ -z "$(bash -lc "echo \$DO_NOT_TRACK")" ]' + +check "~/.config/twg is not symlinked to the volume" bash -c '[ ! -L /home/vscode/.config/twg ]' + +check "xdg-open shim not installed" bash -c '[ ! -e /usr/local/bin/xdg-open ]' + +reportResults diff --git a/test/atlassian-teamwork-graph-cli/scenarios.json b/test/atlassian-teamwork-graph-cli/scenarios.json new file mode 100644 index 0000000..9eb8f9d --- /dev/null +++ b/test/atlassian-teamwork-graph-cli/scenarios.json @@ -0,0 +1,28 @@ +{ + "version": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "atlassian-teamwork-graph-cli": { + "version": "1.3.5" + } + } + }, + "skills": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "atlassian-teamwork-graph-cli": { + "installSkills": true + } + } + }, + "opt-outs": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "atlassian-teamwork-graph-cli": { + "enableTelemetry": true, + "persistAuth": false, + "configureBrowser": false + } + } + } +} diff --git a/test/atlassian-teamwork-graph-cli/skills.sh b/test/atlassian-teamwork-graph-cli/skills.sh new file mode 100644 index 0000000..bc3b5d0 --- /dev/null +++ b/test/atlassian-teamwork-graph-cli/skills.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Scenario 'skills': installSkills=true installs TWG agent skills for the remote user. + +set -e + +source dev-container-features-test-lib + +check "canonical twg skill installed" test -f /home/vscode/.agents/skills/twg/SKILL.md + +check "claude twg skill installed" test -f /home/vscode/.claude/skills/twg/SKILL.md + +check "skills owned by the remote user" bash -c '[ "$(stat -c %U /home/vscode/.agents/skills/twg/SKILL.md)" = vscode ]' + +reportResults diff --git a/test/atlassian-teamwork-graph-cli/test.sh b/test/atlassian-teamwork-graph-cli/test.sh new file mode 100644 index 0000000..1c31f8e --- /dev/null +++ b/test/atlassian-teamwork-graph-cli/test.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# This test file will be executed against an auto-generated devcontainer.json that +# includes the 'atlassian-teamwork-graph-cli' Feature with no options, so every +# option falls back to its default in 'devcontainer-feature.json'. + +set -e + +source dev-container-features-test-lib + +USER_HOME="$(getent passwd "$(id -un)" | cut -d: -f6)" + +check "twg is on PATH" command -v twg + +check "/usr/local/bin/twg symlinks to the user-local binary" bash -c '[ -L /usr/local/bin/twg ] && [ -x "$(readlink /usr/local/bin/twg)" ]' + +check "twg version command works" bash -c "twg -v 2>&1 | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'" + +check "twg jira command available" bash -c "twg jira --help 2>&1 | grep -qi 'jira'" + +check "twg doctor runs without login" bash -c "twg doctor 2>&1 | grep -q 'Version:'" + +# enableTelemetry defaults to false +check "telemetry env file disables tracking" grep -q 'DO_NOT_TRACK=1' /etc/atlassian-teamwork-graph-cli/env.sh + +check "login shells get DO_NOT_TRACK" bash -c '[ "$(bash -lc "echo \$DO_NOT_TRACK")" = "1" ]' + +check "containerEnv BASH_ENV points at the hook" bash -c '[ "$BASH_ENV" = "/etc/atlassian-teamwork-graph-cli/bash_env" ]' + +check "clean non-interactive bash gets telemetry vars via BASH_ENV" bash -c 'out=$(env -i PATH="$PATH" BASH_ENV=/etc/atlassian-teamwork-graph-cli/bash_env bash -c "echo \$DO_NOT_TRACK:\$TWG_DISABLE_3P_DATA_TELEMETRY"); [ "$out" = "1:1" ]' + +# persistAuth defaults to true +check "~/.config/twg is symlinked to the volume" bash -c "[ \"\$(readlink '${USER_HOME}/.config/twg')\" = /twg-config ]" + +check "twg config volume is writable" test -w /twg-config + +# installSkills defaults to false +check "skills not installed by default" bash -c "[ ! -e '${USER_HOME}/.agents/skills/twg' ]" + +check "xdg-open shim installed" test -x /usr/local/bin/xdg-open + +check "xdg-open shim prefers \$BROWSER" bash -c ' + marker="$(mktemp)" + fake="$(mktemp)" + printf "#!/bin/sh\necho \"\$1\" > \"%s\"\n" "$marker" > "$fake" + chmod +x "$fake" + BROWSER="$fake" /usr/local/bin/xdg-open "https://example.com/oauth" >/dev/null 2>&1 + grep -q "https://example.com/oauth" "$marker" +' + +reportResults diff --git a/test/atlassian-teamwork-graph-cli/version.sh b/test/atlassian-teamwork-graph-cli/version.sh new file mode 100644 index 0000000..b23202e --- /dev/null +++ b/test/atlassian-teamwork-graph-cli/version.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Scenario 'version': pinned version, installed for the non-root remote user. + +set -e + +source dev-container-features-test-lib + +check "pinned twg version installed" bash -c "twg -v 2>&1 | grep -q '^1\.3\.5'" + +check "binary installed in the remote user's ~/.local/bin" test -x /home/vscode/.local/bin/twg + +check "binary owned by the remote user" bash -c '[ "$(stat -c %U /home/vscode/.local/bin/twg)" = vscode ]' + +check "~/.local/bin owned by the remote user" bash -c '[ "$(stat -c %U /home/vscode/.local/bin)" = vscode ]' + +check "/usr/local/bin/twg symlinks to the user binary" bash -c '[ "$(readlink /usr/local/bin/twg)" = /home/vscode/.local/bin/twg ]' + +check "twg config volume owned by the remote user" bash -c '[ "$(stat -c %U /twg-config)" = vscode ]' + +reportResults