diff --git a/.github/workflows/dogfood.yaml b/.github/workflows/dogfood.yaml index 1d71d72a9d..2a7919d458 100644 --- a/.github/workflows/dogfood.yaml +++ b/.github/workflows/dogfood.yaml @@ -125,6 +125,19 @@ jobs: tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:26.04" if: matrix.image-version == '26.04' + - name: Build and push vscode-coder image + uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0 + with: + project: b4q6ltmpzh + token: ${{ secrets.DEPOT_TOKEN }} + buildx-fallback: true + context: "{{defaultContext}}:dogfood/vscode-coder" + pull: true + save: true + push: ${{ github.ref == 'refs/heads/main' }} + tags: "codercom/oss-dogfood-vscode-coder:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood-vscode-coder:latest" + if: matrix.image-version == '22.04' + - name: Build Nix image run: nix build .#dev_image if: matrix.image-version == 'nix' @@ -184,6 +197,10 @@ jobs: terraform init terraform validate popd + pushd dogfood/vscode-coder + terraform init + terraform validate + popd - name: Get short commit SHA if: github.ref == 'refs/heads/main' diff --git a/dogfood/main.tf b/dogfood/main.tf index 1074c805c0..4e9442f547 100644 --- a/dogfood/main.tf +++ b/dogfood/main.tf @@ -94,6 +94,53 @@ resource "coderd_template" "dogfood" { } +resource "coderd_template" "vscode_coder" { + name = "vscode-coder" + display_name = "Write VS Code Extension on Coder" + description = "Develop the coder/vscode-coder VS Code extension on Coder." + icon = "/icon/code.svg" + organization_id = data.coderd_organization.default.id + versions = [ + { + name = var.CODER_TEMPLATE_VERSION + message = var.CODER_TEMPLATE_MESSAGE + directory = "./vscode-coder" + active = true + tf_vars = [ + { + name = "anthropic_api_key" + value = var.CODER_DOGFOOD_ANTHROPIC_API_KEY + } + ] + } + ] + acl = { + groups = [{ + id = data.coderd_organization.default.id + role = "use" + }] + users = [{ + id = data.coderd_user.machine.id + role = "admin" + }] + } + activity_bump_ms = 10800000 + allow_user_auto_start = true + allow_user_auto_stop = true + allow_user_cancel_workspace_jobs = false + auto_start_permitted_days_of_week = ["friday", "monday", "saturday", "sunday", "thursday", "tuesday", "wednesday"] + auto_stop_requirement = { + days_of_week = ["sunday"] + weeks = 1 + } + default_ttl_ms = 28800000 + deprecation_message = null + failure_ttl_ms = 604800000 + require_active_version = true + time_til_dormant_autodelete_ms = 7776000000 + time_til_dormant_ms = 8640000000 +} + resource "coderd_template" "envbuilder_dogfood" { name = "coder-envbuilder" display_name = "Write Coder on Coder using Envbuilder" diff --git a/dogfood/vscode-coder/Dockerfile b/dogfood/vscode-coder/Dockerfile new file mode 100644 index 0000000000..134afb4aae --- /dev/null +++ b/dogfood/vscode-coder/Dockerfile @@ -0,0 +1,33 @@ +FROM node:24-slim@sha256:879b21aec4a1ad820c27ccd565e7c7ed955f24b92e6694556154f251e4bdb240 + +ARG DEBIAN_FRONTEND=noninteractive + +# Electron/Chromium system libs are installed at startup via +# `playwright install-deps chromium` so they track the project's +# Electron version automatically. +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl dbus git jq sudo openssh-server screen \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# gh CLI from releases (apt repo is unreliable, see cli/cli#6175). +RUN GH_CLI_VERSION=$(curl -s "https://api.github.com/repos/cli/cli/releases/latest" \ + | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') && \ + curl -L "https://github.com/cli/cli/releases/download/v${GH_CLI_VERSION}/gh_${GH_CLI_VERSION}_linux_amd64.deb" -o /tmp/gh.deb && \ + dpkg -i /tmp/gh.deb && rm /tmp/gh.deb + +# pnpm version is controlled by the project's packageManager field. +RUN corepack enable + +RUN echo 'coder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/nopasswd && \ + chmod 640 /etc/sudoers.d/nopasswd + +# Replace the default node:24-slim 'node' user with 'coder' (uid 1000). +RUN userdel -r node && \ + useradd coder --create-home --shell=/bin/bash --uid=1000 --user-group + +RUN ln -s /var/tmp/coder/coder-cli/coder /usr/local/bin/coder && \ + ln -s /var/tmp/coder/code-server/bin/code-server /usr/local/bin/code-server + +RUN echo "PermitUserEnvironment yes" >> /etc/ssh/sshd_config + +USER coder diff --git a/dogfood/vscode-coder/main.tf b/dogfood/vscode-coder/main.tf new file mode 100644 index 0000000000..17c4316cc5 --- /dev/null +++ b/dogfood/vscode-coder/main.tf @@ -0,0 +1,620 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13.0" + } + docker = { + source = "kreuzwerker/docker" + version = "~> 4.0" + } + } +} + +locals { + // These are cluster service addresses mapped to Tailscale nodes. + // Ask Dean or Kyle for help. + docker_host = { + "" = "tcp://rubinsky-pit-cdr-dev.tailscale.svc.cluster.local:2375" + "us-pittsburgh" = "tcp://rubinsky-pit-cdr-dev.tailscale.svc.cluster.local:2375" + // For legacy reasons, this host is labelled `eu-helsinki` but it's + // actually in Germany now. + "eu-helsinki" = "tcp://katerose-fsn-cdr-dev.tailscale.svc.cluster.local:2375" + "ap-sydney" = "tcp://wolfgang-syd-cdr-dev.tailscale.svc.cluster.local:2375" + "za-cpt" = "tcp://schonkopf-cpt-cdr-dev.tailscale.svc.cluster.local:2375" + } + + repo_base_dir = data.coder_parameter.repo_base_dir.value == "~" ? "/home/coder" : replace(data.coder_parameter.repo_base_dir.value, "/^~\\//", "/home/coder/") + repo_dir = replace(try(module.git-clone[0].repo_dir, ""), "/^~\\//", "/home/coder/") + container_name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}" +} + +# --- Parameters --- + +data "coder_parameter" "repo_base_dir" { + type = "string" + name = "Repository Base Directory" + default = "~" + description = "The directory specified will be created (if missing) and [coder/vscode-coder](https://github.com/coder/vscode-coder) will be automatically cloned into [base directory]/vscode-coder." + mutable = true +} + +locals { + default_regions = { + "north-america" : "us-pittsburgh" + "europe" : "eu-helsinki" + "australia" : "ap-sydney" + "africa" : "za-cpt" + } + + user_groups = data.coder_workspace_owner.me.groups + user_region = coalescelist([ + for g in local.user_groups : + local.default_regions[g] if contains(keys(local.default_regions), g) + ], ["us-pittsburgh"])[0] +} + +data "coder_parameter" "region" { + type = "string" + name = "Region" + icon = "/emojis/1f30e.png" + default = local.user_region + option { + icon = "/emojis/1f1fa-1f1f8.png" + name = "Pittsburgh" + value = "us-pittsburgh" + } + option { + icon = "/emojis/1f1e9-1f1ea.png" + name = "Falkenstein" + // For legacy reasons, this host is labelled `eu-helsinki` but it's + // actually in Germany now. + value = "eu-helsinki" + } + option { + icon = "/emojis/1f1e6-1f1fa.png" + name = "Sydney" + value = "ap-sydney" + } + option { + icon = "/emojis/1f1ff-1f1e6.png" + name = "Cape Town" + value = "za-cpt" + } +} + +data "coder_parameter" "res_mon_memory_threshold" { + type = "number" + name = "Memory usage threshold" + default = 80 + description = "The memory usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_threshold" { + type = "number" + name = "Volume usage threshold" + default = 90 + description = "The volume usage threshold used in resources monitoring to trigger notifications." + mutable = true + validation { + min = 0 + max = 100 + } +} + +data "coder_parameter" "res_mon_volume_path" { + type = "string" + name = "Volume path" + default = "/home/coder" + description = "The path monitored in resources monitoring to trigger notifications." + mutable = true +} + +data "coder_parameter" "use_ai_bridge" { + type = "bool" + name = "Use AI Bridge" + default = true + description = "If enabled, AI requests will be sent via AI Bridge." + mutable = true +} + +# Fallback when AI Bridge is disabled. Injected by dogfood/main.tf +# from the CODER_DOGFOOD_ANTHROPIC_API_KEY secret. +variable "anthropic_api_key" { + type = string + description = "Anthropic API key, used when AI Bridge is disabled." + default = "" + sensitive = true +} + +data "coder_parameter" "ide_choices" { + type = "list(string)" + name = "Select IDEs" + form_type = "multi-select" + mutable = true + description = "Choose one or more IDEs to enable in your workspace" + default = jsonencode(["vscode", "code-server", "cursor"]) + option { + name = "VS Code Desktop" + value = "vscode" + icon = "/icon/code.svg" + } + option { + name = "code-server" + value = "code-server" + icon = "/icon/code.svg" + } + option { + name = "VS Code Web" + value = "vscode-web" + icon = "/icon/code.svg" + } + option { + name = "Cursor" + value = "cursor" + icon = "/icon/cursor.svg" + } + option { + name = "Windsurf" + value = "windsurf" + icon = "/icon/windsurf.svg" + } + option { + name = "Zed" + value = "zed" + icon = "/icon/zed.svg" + } +} + +data "coder_parameter" "vscode_channel" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode") ? 1 : 0 + type = "string" + name = "VS Code Desktop channel" + description = "Choose the VS Code Desktop channel" + mutable = true + default = "stable" + option { + value = "stable" + name = "Stable" + icon = "/icon/code.svg" + } + option { + value = "insiders" + name = "Insiders" + icon = "/icon/code-insiders.svg" + } +} + +# --- Providers and data sources --- + +provider "docker" { + host = lookup(local.docker_host, data.coder_parameter.region.value) +} + +provider "coder" {} + +data "coder_external_auth" "github" { + id = "github" +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} +data "coder_task" "me" {} +data "coder_workspace_tags" "tags" { + tags = { + "cluster" : "dogfood-v2" + "env" : "gke" + } +} + +# --- Modules --- + +module "dotfiles" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/dotfiles/coder" + version = "1.4.1" + agent_id = coder_agent.dev.id +} + +module "git-config" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/git-config/coder" + version = "1.0.33" + agent_id = coder_agent.dev.id + allow_email_change = true +} + +module "git-clone" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/git-clone/coder" + version = "1.2.3" + agent_id = coder_agent.dev.id + url = "https://github.com/coder/vscode-coder" + base_dir = local.repo_base_dir + post_clone_script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + coder exp sync start git-clone + coder exp sync complete git-clone + EOT +} + +module "personalize" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/personalize/coder" + version = "1.0.32" + agent_id = coder_agent.dev.id +} + +module "code-server" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "code-server") ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/code-server/coder" + version = "1.4.4" + agent_id = coder_agent.dev.id + folder = local.repo_dir + auto_install_extensions = true + group = "Web Editors" +} + +module "vscode-web" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode-web") ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/vscode-web/coder" + version = "1.5.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir + extensions = ["github.copilot"] + auto_install_extensions = true + accept_license = true + group = "Web Editors" +} + +module "filebrowser" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/filebrowser/coder" + version = "1.1.4" + agent_id = coder_agent.dev.id + agent_name = "dev" +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "dev.registry.coder.com/coder/coder-login/coder" + version = "1.1.1" + agent_id = coder_agent.dev.id +} + +module "cursor" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "cursor") ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/cursor/coder" + version = "1.4.1" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +module "windsurf" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "windsurf") ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/windsurf/coder" + version = "1.3.1" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + +module "zed" { + count = contains(jsondecode(data.coder_parameter.ide_choices.value), "zed") ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/zed/coder" + version = "1.1.4" + agent_id = coder_agent.dev.id + agent_name = "dev" + folder = local.repo_dir +} + +# --- Agent --- + +resource "coder_agent" "dev" { + arch = "amd64" + os = "linux" + dir = local.repo_dir + env = merge( + { + OIDC_TOKEN : data.coder_workspace_owner.me.oidc_access_token, + }, + data.coder_parameter.use_ai_bridge.value ? { + ANTHROPIC_BASE_URL : "https://dev.coder.com/api/v2/aibridge/anthropic", + ANTHROPIC_AUTH_TOKEN : data.coder_workspace_owner.me.session_token, + OPENAI_BASE_URL : "https://dev.coder.com/api/v2/aibridge/openai/v1", + OPENAI_API_KEY : data.coder_workspace_owner.me.session_token, + } : {} + ) + startup_script_behavior = "blocking" + + display_apps { + vscode = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode") && try(data.coder_parameter.vscode_channel[0].value, "stable") == "stable" + vscode_insiders = contains(jsondecode(data.coder_parameter.ide_choices.value), "vscode") && try(data.coder_parameter.vscode_channel[0].value, "stable") == "insiders" + } + + metadata { + display_name = "CPU Usage" + key = "cpu_usage" + order = 0 + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "ram_usage" + order = 1 + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "/home Usage" + key = "home_usage" + order = 2 + script = "sudo du -sh /home/coder | awk '{print $1}'" + interval = 3600 + timeout = 60 + } + + resources_monitoring { + memory { + enabled = true + threshold = data.coder_parameter.res_mon_memory_threshold.value + } + volume { + enabled = true + threshold = data.coder_parameter.res_mon_volume_threshold.value + path = data.coder_parameter.res_mon_volume_path.value + } + } + + startup_script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + + function cleanup() { + coder exp sync complete agent-startup + touch /tmp/.coder-startup-script.done + } + trap cleanup EXIT + coder exp sync start agent-startup + + # Start dbus to suppress noisy Electron/Chromium errors in tests. + sudo mkdir -p /run/dbus + sudo dbus-daemon --system 2>/dev/null || true + + if ! gh api user --jq .login >/dev/null 2>&1; then + echo "Logging into GitHub CLI..." + if ! coder external-auth access-token github | gh auth login --hostname github.com --with-token; then + echo "GitHub CLI authentication failed; gh commands may not work." + fi + else + echo "GitHub CLI already has working credentials." + fi + EOT +} + +# --- Scripts --- + +resource "coder_script" "install-deps" { + agent_id = coder_agent.dev.id + display_name = "Installing Dependencies" + run_on_start = true + start_blocks_login = false + script = <`. + 2. Create a feature branch for the work using a descriptive name + based on the issue or task. + Example: `git checkout -b fix/issue-123-ssh-retry` + 3. Proceed with implementation following the AGENTS.md guidelines. + + -- Context -- + This is the coder/vscode-coder VS Code extension. It is a real-world + production extension used by developers to connect to Coder workspaces. + Be sure to read AGENTS.md before making any changes. + EOT +} + +module "claude-code" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + source = "dev.registry.coder.com/coder/claude-code/coder" + version = "4.9.2" + enable_boundary = true + agent_id = coder_agent.dev.id + workdir = local.repo_dir + claude_code_version = "latest" + model = "opus" + order = 999 + claude_api_key = data.coder_parameter.use_ai_bridge.value ? data.coder_workspace_owner.me.session_token : var.anthropic_api_key + agentapi_version = "latest" + system_prompt = local.claude_system_prompt + ai_prompt = data.coder_task.me.prompt +} + +resource "coder_ai_task" "task" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + app_id = module.claude-code[count.index].task_app_id +} + +resource "coder_app" "watch" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + agent_id = coder_agent.dev.id + slug = "watch" + display_name = "pnpm watch" + icon = "${data.coder_workspace.me.access_url}/icon/code.svg" + command = "screen -x pnpm_watch" + share = "authenticated" + open_in = "tab" + order = 0 +} + +resource "coder_script" "watch" { + count = data.coder_task.me.enabled ? data.coder_workspace.me.start_count : 0 + display_name = "pnpm watch" + agent_id = coder_agent.dev.id + run_on_start = true + start_blocks_login = false + icon = "${data.coder_workspace.me.access_url}/icon/code.svg" + script = <<-EOT + #!/usr/bin/env bash + set -eux -o pipefail + + trap 'coder exp sync complete pnpm-watch' EXIT + coder exp sync want pnpm-watch install-deps + coder exp sync start pnpm-watch + + cd "${local.repo_dir}" && screen -dmS pnpm_watch /bin/sh -c 'while true; do pnpm watch; echo "pnpm watch exited with code $? restarting in 10s"; sleep 10; done' + EOT +}