From 95efe242ac44371fbe0eeeb0d72acf50136dc1e1 Mon Sep 17 00:00:00 2001 From: Alexandre Pires Date: Mon, 6 Jul 2026 17:04:24 +0200 Subject: [PATCH] Add GitHub Copilot installation support to macOS development station --- .../roles/macos_dev_station/defaults/main.yml | 6 + .../files/install-copilot.sh | 164 ++++++++++++++++++ .../roles/macos_dev_station/tasks/main.yml | 29 ++++ 3 files changed, 199 insertions(+) create mode 100755 ansible/roles/macos_dev_station/files/install-copilot.sh diff --git a/ansible/roles/macos_dev_station/defaults/main.yml b/ansible/roles/macos_dev_station/defaults/main.yml index 5d55b66..16e53ab 100644 --- a/ansible/roles/macos_dev_station/defaults/main.yml +++ b/ansible/roles/macos_dev_station/defaults/main.yml @@ -31,6 +31,12 @@ macos_dev_station_code_server_extensions: - redhat.ansible - eamodio.gitlens +macos_dev_station_copilot_enabled: false +macos_dev_station_copilot_extensions: + - GitHub.copilot + - GitHub.copilot-chat +macos_dev_station_copilot_fail_on_error: false + macos_dev_station_oh_my_zsh_enabled: true macos_dev_station_oh_my_zsh_install_url: "https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" diff --git a/ansible/roles/macos_dev_station/files/install-copilot.sh b/ansible/roles/macos_dev_station/files/install-copilot.sh new file mode 100755 index 0000000..5c355fa --- /dev/null +++ b/ansible/roles/macos_dev_station/files/install-copilot.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# ansible: macos_dev_station install-copilot.sh +# Installs GitHub Copilot and Copilot Chat extensions into code-server +# with engine-compatible versions resolved from the VS Marketplace API. +set -euo pipefail + +get_engine_version() { + local version_output + version_output=$(code-server --version | head -n1) + if echo "$version_output" | grep -q "with Code"; then + echo "$version_output" | sed -n 's/.*with Code \([0-9.]*\).*/\1/p' + else + echo "$version_output" | awk '{print $1}' + fi +} + +get_user_data_dir() { + local process_info + process_info=$(ps aux 2>/dev/null | grep -v grep | grep "code-server" | head -n 1) || + process_info=$(ps -ef 2>/dev/null | grep -v grep | grep "code-server" | head -n 1) + if [ -n "$process_info" ]; then + local dir + dir=$(echo "$process_info" | grep -o -- '--user-data-dir=[^ ]*' | sed 's/--user-data-dir=//') + if [ -n "$dir" ]; then + echo "$dir"; return + fi + dir=$(echo "$process_info" | grep -o -- '--user-data-dir [^ ]*' | sed 's/--user-data-dir //') + if [ -n "$dir" ]; then + echo "$dir"; return + fi + fi +} + +find_compatible_version() { + local extension_id="$1" + local engine_version="$2" + local response + response=$(curl -s -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json;api-version=3.0-preview.1" \ + -d "{ + \"filters\": [{ + \"criteria\": [ + {\"filterType\": 7, \"value\": \"$extension_id\"}, + {\"filterType\": 12, \"value\": \"4096\"} + ], + \"pageSize\": 50 + }], + \"flags\": 4112 + }") + echo "$response" | jq -r --arg engine_version "$engine_version" ' + .results[0].extensions[0].versions[] | + select(.version | test("^[0-9]+\\.[0-9]+\\.[0-9]*$")) | + select(all(.properties[]; .key != "Microsoft.VisualStudio.Code.PreRelease" or .value != "true")) | + { + version: .version, + engine: (.properties[] | select(.key == "Microsoft.VisualStudio.Code.Engine") | .value) + } | + select(.engine | ltrimstr("^") | split(".") | + map(split("-")[0] | tonumber? // 0) as $engine_parts | + ($engine_version | split(".") | map(split("-")[0] | tonumber? // 0)) as $server_parts | + ( + ($engine_parts[0] // 0) < $server_parts[0] or + (($engine_parts[0] // 0) == $server_parts[0] and ($engine_parts[1] // 0) < $server_parts[1]) or + (($engine_parts[0] // 0) == $server_parts[0] and ($engine_parts[1] // 0) == $server_parts[1] and ($engine_parts[2] // 0) <= $server_parts[2]) + ) + ) | + .version' | head -n 1 +} + +install_extension() { + local extension_id="$1" + local version="$2" + local user_data_dir="$3" + local extension_name + extension_name=$(echo "$extension_id" | cut -d'.' -f2) + local temp_dir="/tmp/code-extensions" + echo "Installing $extension_id v$version..." + mkdir -p "$temp_dir" + echo " Downloading..." + curl -L "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/GitHub/vsextensions/$extension_name/$version/vspackage" \ + -o "$temp_dir/$extension_name.vsix.gz" + if [ ! -f "$temp_dir/$extension_name.vsix.gz" ]; then + echo " [SKIP] Download failed for $extension_id" + return 1 + fi + if command -v gunzip >/dev/null 2>&1; then + gunzip -f "$temp_dir/$extension_name.vsix.gz" + else + gzip -df "$temp_dir/$extension_name.vsix.gz" + fi + if [ -n "$user_data_dir" ]; then + code-server --user-data-dir="$user_data_dir" --force --install-extension "$temp_dir/$extension_name.vsix" + else + code-server --force --install-extension "$temp_dir/$extension_name.vsix" + fi + rm -f "$temp_dir/$extension_name.vsix" + echo " [OK] $extension_id installed successfully!" + return 0 +} + +check_dependencies() { + local missing_deps=() + for cmd in curl jq code-server; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing_deps+=("$cmd") + fi + done + if ! command -v gunzip >/dev/null 2>&1 && ! command -v gzip >/dev/null 2>&1; then + missing_deps+=("gunzip/gzip") + fi + if [ "${#missing_deps[@]}" -gt 0 ]; then + echo "Error: Missing required dependencies: ${missing_deps[*]}" + echo "Please install the missing dependencies and try again." + exit 1 + fi +} + +echo "========================================" +echo " GitHub Copilot Extensions Installer" +echo "========================================" +echo "" + +check_dependencies + +ENGINE_VERSION="$(get_engine_version)" +if [ -z "$ENGINE_VERSION" ]; then + echo "Error: Could not extract engine version from code-server" + exit 1 +fi +echo "Detected code-server engine version: $ENGINE_VERSION" + +USER_DATA_DIR="$(get_user_data_dir)" +if [ -n "$USER_DATA_DIR" ]; then + echo "Detected user-data-dir: $USER_DATA_DIR" +fi +echo "" + +EXTENSIONS="GitHub.copilot GitHub.copilot-chat" +FAILED=0 + +for ext in $EXTENSIONS; do + echo "Processing $ext..." + version="$(find_compatible_version "$ext" "$ENGINE_VERSION")" + if [ -z "$version" ]; then + echo " [SKIP] No compatible version found for $ext" + FAILED="$((FAILED + 1))" + else + echo " Found compatible version: $version" + if ! install_extension "$ext" "$version" "$USER_DATA_DIR"; then + FAILED="$((FAILED + 1))" + fi + fi + echo "" +done + +echo "========================================" +if [ $FAILED -eq 0 ]; then + echo "[OK] All extensions installed successfully!" + rm -rf /tmp/code-extensions +else + echo "[WARN] Completed with $FAILED error(s)" + exit 1 +fi diff --git a/ansible/roles/macos_dev_station/tasks/main.yml b/ansible/roles/macos_dev_station/tasks/main.yml index 654efd4..43e319d 100644 --- a/ansible/roles/macos_dev_station/tasks/main.yml +++ b/ansible/roles/macos_dev_station/tasks/main.yml @@ -270,6 +270,35 @@ - item.rc is defined - item.rc != 0 +- name: Copy Copilot installer script + become: true + ansible.builtin.copy: + src: install-copilot.sh + dest: "{{ macos_dev_station_service_home }}/.cache/install-copilot.sh" + owner: "{{ macos_dev_station_service_user }}" + group: "{{ macos_dev_station_service_group }}" + mode: "0755" + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_copilot_enabled | bool + +- name: Install GitHub Copilot extensions via compatible VSIX + become: true + become_user: "{{ macos_dev_station_service_user }}" + ansible.builtin.command: "{{ macos_dev_station_service_home }}/.cache/install-copilot.sh" + register: __copilot_install_result + changed_when: __copilot_install_result.rc == 0 + failed_when: >- + (macos_dev_station_copilot_fail_on_error | bool) + and (__copilot_install_result.rc != 0) + environment: + HOME: "{{ macos_dev_station_service_home }}" + USER: "{{ macos_dev_station_service_user }}" + PATH: "{{ macos_dev_station_path }}" + when: + - macos_dev_station_service_user_enabled | bool + - macos_dev_station_copilot_enabled | bool + - name: Ensure devstation SSH directory exists when SSH access is enabled become: true ansible.builtin.file: