mirror of
https://github.com/coder/registry.git
synced 2026-06-04 21:48:16 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9b223ac3c | |||
| 1749f9ca05 | |||
| 61554aaa8c | |||
| f4fcae7c0f | |||
| 05b9bb1ae4 | |||
| 45b72c7241 | |||
| 2646b36cb1 | |||
| 3202e4899a | |||
| c4a5184725 | |||
| 63d56eadc9 |
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.34.0
|
||||
uses: crate-ci/typos@v1.35.3
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@6a7c903a70c8625ed6700fa299f5ddb4ca6022e9
|
||||
uses: google-github-actions/setup-gcloud@cb1e50a9932213ecece00a606661ae9ca44f3397
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch main
|
||||
- name: Deploy to registry.coder.com
|
||||
|
||||
+16
-11
@@ -24,7 +24,7 @@ The Coder Registry is a collection of Terraform modules and templates for Coder
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
Install Bun:
|
||||
Install Bun (for formatting and scripts):
|
||||
|
||||
```bash
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
@@ -124,19 +124,23 @@ This script generates:
|
||||
- Accurate description and usage examples
|
||||
- Correct icon path (usually `../../../../.icons/your-icon.svg`)
|
||||
- Proper tags that describe your module
|
||||
3. **Create `main.test.ts`** to test your module
|
||||
3. **Create at least one `.tftest.hcl`** to test your module with `terraform test`
|
||||
4. **Add any scripts** or additional files your module needs
|
||||
|
||||
### 4. Test and Submit
|
||||
|
||||
```bash
|
||||
# Test your module
|
||||
bun test -t 'module-name'
|
||||
# Test your module (from the module directory)
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Or run all tests in the repo
|
||||
./scripts/terraform_test_all.sh
|
||||
|
||||
# Format code
|
||||
bun fmt
|
||||
bun run fmt
|
||||
|
||||
# Commit and create PR
|
||||
# Commit and create PR (do not push to main directly)
|
||||
git add .
|
||||
git commit -m "Add [module-name] module"
|
||||
git push origin your-branch
|
||||
@@ -335,11 +339,12 @@ coder templates push test-[template-name] -d .
|
||||
### 2. Test Your Changes
|
||||
|
||||
```bash
|
||||
# Test a specific module
|
||||
bun test -t 'module-name'
|
||||
# Test a specific module (from the module directory)
|
||||
terraform init -upgrade
|
||||
terraform test -verbose
|
||||
|
||||
# Test all modules
|
||||
bun test
|
||||
./scripts/terraform_test_all.sh
|
||||
```
|
||||
|
||||
### 3. Maintain Backward Compatibility
|
||||
@@ -388,7 +393,7 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template=
|
||||
### Every Module Must Have
|
||||
|
||||
- `main.tf` - Terraform code
|
||||
- `main.test.ts` - Working tests
|
||||
- One or more `.tftest.hcl` files - Working tests with `terraform test`
|
||||
- `README.md` - Documentation with frontmatter
|
||||
|
||||
### Every Template Must Have
|
||||
@@ -488,6 +493,6 @@ When reporting bugs, include:
|
||||
2. **No tests** or broken tests
|
||||
3. **Hardcoded values** instead of variables
|
||||
4. **Breaking changes** without defaults
|
||||
5. **Not running** `bun fmt` before submitting
|
||||
5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting
|
||||
|
||||
Happy contributing! 🚀
|
||||
|
||||
+2
-2
@@ -18,9 +18,9 @@ sudo apt install golang-go
|
||||
|
||||
Check that PRs have:
|
||||
|
||||
- [ ] All required files (`main.tf`, `main.test.ts`, `README.md`)
|
||||
- [ ] All required files (`main.tf`, `README.md`, at least one `.tftest.hcl`)
|
||||
- [ ] Proper frontmatter in README
|
||||
- [ ] Working tests (`bun test`)
|
||||
- [ ] Working tests (`terraform test`)
|
||||
- [ ] Formatted code (`bun run fmt`)
|
||||
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
run "plan_with_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
}
|
||||
}
|
||||
|
||||
run "app_url_uses_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "example-agent-id"
|
||||
port = 19999
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999"
|
||||
error_message = "Expected MODULE_NAME app URL to include configured port"
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff",
|
||||
"fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff",
|
||||
"terraform-validate": "./scripts/terraform_validate.sh",
|
||||
"test": "bun test",
|
||||
"test": "./scripts/terraform_test_all.sh",
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
---
|
||||
display_name: Gemini CLI
|
||||
description: Run Gemini CLI in your workspace for AI pair programming
|
||||
icon: ../../../../.icons/gemini.svg
|
||||
description: Run Gemini CLI in your workspace with AgentAPI integration
|
||||
verified: true
|
||||
tags: [agent, gemini, ai, google, tasks]
|
||||
---
|
||||
|
||||
# Gemini CLI
|
||||
|
||||
Run [Gemini CLI](https://ai.google.dev/gemini-api/docs/cli) in your workspace to access Google's Gemini AI models, and custom pre/post install scripts. This module integrates with [AgentAPI](https://github.com/coder/agentapi) for Coder Tasks compatibility.
|
||||
Run [Gemini CLI](https://github.com/google-gemini/gemini-cli) in your workspace to access Google's Gemini AI models for interactive coding assistance and automated task execution.
|
||||
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-pro"
|
||||
install_gemini = true
|
||||
gemini_version = "latest"
|
||||
agentapi_version = "latest"
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive AI Assistance**: Run Gemini CLI directly in your terminal for coding help
|
||||
- **Automated Task Execution**: Execute coding tasks automatically via AgentAPI integration
|
||||
- **Multiple AI Models**: Support for Gemini 2.5 Pro, Flash, and other Google AI models
|
||||
- **API Key Integration**: Seamless authentication with Gemini API
|
||||
- **MCP Server Integration**: Built-in Coder MCP server for task reporting
|
||||
- **Persistent Sessions**: Maintain context across workspace sessions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login/coder) module to your template
|
||||
- Node.js and npm will be installed automatically if not present
|
||||
- The [Coder Login](https://registry.coder.com/modules/coder/coder-login) module is required
|
||||
|
||||
## Usage Example
|
||||
## Examples
|
||||
|
||||
- Example 1:
|
||||
### Basic setup
|
||||
|
||||
```tf
|
||||
variable "gemini_api_key" {
|
||||
@@ -40,39 +45,97 @@ variable "gemini_api_key" {
|
||||
}
|
||||
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key # we recommend providing this parameter inorder to have a smoother experience (i.e. no google sign-in)
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
install_gemini = true
|
||||
gemini_version = "latest"
|
||||
gemini_instruction_prompt = "Start every response with `Gemini says:`"
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
## How it Works
|
||||
This basic setup will:
|
||||
|
||||
- **Install**: The module installs Gemini CLI using npm (installs Node.js via NVM if needed)
|
||||
- **Instruction Prompt**: If `GEMINI_INSTRUCTION_PROMPT` and `GEMINI_START_DIRECTORY` are set, creates the directory (if needed) and writes the prompt to `GEMINI.md`
|
||||
- **Start**: Launches Gemini CLI in the specified directory, wrapped by AgentAPI
|
||||
- **Environment**: Sets `GEMINI_API_KEY`, `GOOGLE_GENAI_USE_VERTEXAI`, `GEMINI_MODEL` for the CLI (if variables provided)
|
||||
- Install Gemini CLI in the workspace
|
||||
- Configure authentication with your API key
|
||||
- Set Gemini to run in `/home/coder/project` directory
|
||||
- Enable interactive use from the terminal
|
||||
- Set up MCP server integration for task reporting
|
||||
|
||||
### Automated task execution (Experimental)
|
||||
|
||||
> This functionality is in early access and is still evolving.
|
||||
> For now, we recommend testing it in a demo or staging environment,
|
||||
> rather than deploying to production
|
||||
>
|
||||
> Learn more in [the Coder documentation](https://coder.com/docs/ai-coder)
|
||||
|
||||
```tf
|
||||
variable "gemini_api_key" {
|
||||
type = string
|
||||
description = "Gemini API key"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "~> 1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Task prompt for automated Gemini execution"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
folder = "/home/coder/project"
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
enable_yolo_mode = true # Auto-approve all tool calls for automation
|
||||
gemini_system_prompt = <<-EOT
|
||||
You are a helpful coding assistant. Always explain your code changes clearly.
|
||||
YOU MUST REPORT ALL TASKS TO CODER.
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> YOLO mode automatically approves all tool calls without user confirmation. The agent has access to your machine's file system and terminal. Only enable in trusted, isolated environments.
|
||||
|
||||
### Using Vertex AI (Enterprise)
|
||||
|
||||
For enterprise users who prefer Google's Vertex AI platform:
|
||||
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
folder = "/home/coder/project"
|
||||
use_vertexai = true
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If Gemini CLI is not found, ensure `install_gemini = true` and your API key is valid
|
||||
- Node.js and npm are installed automatically if missing (using NVM)
|
||||
- Check logs in `/home/coder/.gemini-module/` for install/start output
|
||||
- We highly recommend using the `gemini_api_key` variable, this also ensures smooth tasks running without needing to sign in to Google.
|
||||
- If Gemini CLI is not found, ensure your API key is valid (`install_gemini` defaults to `true`)
|
||||
- Check logs in `~/.gemini-module/` for install/start output
|
||||
- Use the `gemini_api_key` variable to avoid requiring Google sign-in
|
||||
|
||||
> [!IMPORTANT]
|
||||
> To use tasks with Gemini CLI, ensure you have the `gemini_api_key` variable set, and **you pass the `AI Prompt` Parameter**.
|
||||
> By default we inject the "theme": "Default" and "selectedAuthType": "gemini-api-key" to your ~/.gemini/settings.json along with the coder mcp server.
|
||||
> In `gemini_instruction_prompt` and `AI Prompt` text we recommend using (\`\`) backticks instead of quotes to avoid escaping issues. Eg: gemini_instruction_prompt = "Start every response with \`Gemini says:\` "
|
||||
The module creates log files in the workspace's `~/.gemini-module` directory for debugging purposes.
|
||||
|
||||
## References
|
||||
|
||||
- [Gemini CLI Documentation](https://ai.google.dev/gemini-api/docs/cli)
|
||||
- [Gemini CLI Documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/index.md)
|
||||
- [AgentAPI Documentation](https://github.com/coder/agentapi)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents)
|
||||
- [Coder AI Agents Guide](https://coder.com/docs/ai-coder)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
} from "bun:test";
|
||||
import { execContainer, readFileContainer, runTerraformInit } from "~test";
|
||||
import {
|
||||
loadTestFile,
|
||||
writeExecutable,
|
||||
setup as setupUtil,
|
||||
execModuleScript,
|
||||
@@ -54,10 +53,24 @@ const setup = async (props?: SetupProps): Promise<{ id: string }> => {
|
||||
agentapiMockScript: props?.agentapiMockScript,
|
||||
});
|
||||
if (!props?.skipGeminiMock) {
|
||||
const geminiMockContent = `#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "gemini version v2.5.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - gemini-mock"
|
||||
sleep 15
|
||||
done`;
|
||||
await writeExecutable({
|
||||
containerId: id,
|
||||
filePath: "/usr/bin/gemini",
|
||||
content: await loadTestFile(import.meta.dir, "gemini-mock.sh"),
|
||||
content: geminiMockContent,
|
||||
});
|
||||
}
|
||||
return { id };
|
||||
@@ -70,7 +83,7 @@ describe("gemini", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
test("happy-path", async () => {
|
||||
test("agent-api", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
@@ -117,7 +130,7 @@ describe("gemini", async () => {
|
||||
await execModuleScript(id);
|
||||
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
|
||||
expect(resp).toContain("gemini_api_key provided !");
|
||||
expect(resp).toContain("Using direct Gemini API with API key");
|
||||
});
|
||||
|
||||
test("use-vertexai", async () => {
|
||||
@@ -197,6 +210,20 @@ describe("gemini", async () => {
|
||||
expect(resp).toContain(prompt);
|
||||
});
|
||||
|
||||
test("task-prompt", async () => {
|
||||
const taskPrompt = "Create a simple Hello World function";
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
task_prompt: taskPrompt,
|
||||
},
|
||||
});
|
||||
await execModuleScript(id, {
|
||||
GEMINI_TASK_PROMPT: taskPrompt,
|
||||
});
|
||||
const resp = await readFileContainer(id, "/home/coder/.gemini-module/agentapi-start.log");
|
||||
expect(resp).toContain("Running automated task:");
|
||||
});
|
||||
|
||||
test("start-without-prompt", async () => {
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
|
||||
@@ -74,14 +74,14 @@ variable "use_vertexai" {
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
description = "Whether to install AgentAPI for web UI and task automation."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.0"
|
||||
default = "v0.2.3"
|
||||
}
|
||||
|
||||
variable "gemini_model" {
|
||||
@@ -102,12 +102,10 @@ variable "post_install_script" {
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt for automated Gemini execution"
|
||||
default = ""
|
||||
description = "Initial prompt for the Gemini CLI"
|
||||
mutable = true
|
||||
}
|
||||
|
||||
variable "additional_extensions" {
|
||||
@@ -122,12 +120,24 @@ variable "gemini_system_prompt" {
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "enable_yolo_mode" {
|
||||
type = bool
|
||||
description = "Enable YOLO mode to automatically approve all tool calls without user confirmation. Use with caution."
|
||||
default = false
|
||||
}
|
||||
|
||||
resource "coder_env" "gemini_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = "GEMINI_API_KEY"
|
||||
value = var.gemini_api_key
|
||||
}
|
||||
|
||||
resource "coder_env" "google_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = "GOOGLE_API_KEY"
|
||||
value = var.gemini_api_key
|
||||
}
|
||||
|
||||
resource "coder_env" "gemini_use_vertex_ai" {
|
||||
agent_id = var.agent_id
|
||||
name = "GOOGLE_GENAI_USE_VERTEXAI"
|
||||
@@ -166,7 +176,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
@@ -181,22 +191,7 @@ module "agentapi" {
|
||||
agentapi_version = var.agentapi_version
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
GEMINI_API_KEY='${var.gemini_api_key}' \
|
||||
GOOGLE_GENAI_USE_VERTEXAI='${var.use_vertexai}' \
|
||||
GEMINI_MODEL='${var.gemini_model}' \
|
||||
GEMINI_START_DIRECTORY='${var.folder}' \
|
||||
GEMINI_TASK_PROMPT='${base64encode(data.coder_parameter.ai_prompt.value)}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
|
||||
install_script = <<-EOT
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
@@ -209,7 +204,23 @@ module "agentapi" {
|
||||
BASE_EXTENSIONS='${base64encode(replace(local.base_extensions, "'", "'\\''"))}' \
|
||||
ADDITIONAL_EXTENSIONS='${base64encode(replace(var.additional_extensions != null ? var.additional_extensions : "", "'", "'\\''"))}' \
|
||||
GEMINI_START_DIRECTORY='${var.folder}' \
|
||||
GEMINI_INSTRUCTION_PROMPT='${base64encode(var.gemini_system_prompt)}' \
|
||||
GEMINI_SYSTEM_PROMPT='${base64encode(var.gemini_system_prompt)}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
start_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
chmod +x /tmp/start.sh
|
||||
GEMINI_API_KEY='${var.gemini_api_key}' \
|
||||
GOOGLE_API_KEY='${var.gemini_api_key}' \
|
||||
GOOGLE_GENAI_USE_VERTEXAI='${var.use_vertexai}' \
|
||||
GEMINI_YOLO_MODE='${var.enable_yolo_mode}' \
|
||||
GEMINI_MODEL='${var.gemini_model}' \
|
||||
GEMINI_START_DIRECTORY='${var.folder}' \
|
||||
GEMINI_TASK_PROMPT='${var.task_prompt}' \
|
||||
/tmp/start.sh
|
||||
EOT
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
@@ -12,7 +11,7 @@ set -o nounset
|
||||
ARG_GEMINI_CONFIG=$(echo -n "$ARG_GEMINI_CONFIG" | base64 -d)
|
||||
BASE_EXTENSIONS=$(echo -n "$BASE_EXTENSIONS" | base64 -d)
|
||||
ADDITIONAL_EXTENSIONS=$(echo -n "$ADDITIONAL_EXTENSIONS" | base64 -d)
|
||||
GEMINI_INSTRUCTION_PROMPT=$(echo -n "$GEMINI_INSTRUCTION_PROMPT" | base64 -d)
|
||||
GEMINI_SYSTEM_PROMPT=$(echo -n "$GEMINI_SYSTEM_PROMPT" | base64 -d)
|
||||
|
||||
echo "--------------------------------"
|
||||
printf "gemini_config: %s\n" "$ARG_GEMINI_CONFIG"
|
||||
@@ -23,7 +22,6 @@ echo "--------------------------------"
|
||||
set +o nounset
|
||||
|
||||
function install_node() {
|
||||
# borrowed from claude-code module
|
||||
if ! command_exists npm; then
|
||||
printf "npm not found, checking for Node.js installation...\n"
|
||||
if ! command_exists node; then
|
||||
@@ -52,24 +50,15 @@ function install_node() {
|
||||
|
||||
function install_gemini() {
|
||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||
# we need node to install and run gemini-cli
|
||||
install_node
|
||||
|
||||
# If nvm does not exist, we will create a global npm directory (this os to prevent the possibility of EACCESS issues on npm -g)
|
||||
if ! command_exists nvm; then
|
||||
printf "which node: %s\n" "$(which node)"
|
||||
printf "which npm: %s\n" "$(which npm)"
|
||||
|
||||
# Create a directory for global packages
|
||||
mkdir -p "$HOME"/.npm-global
|
||||
|
||||
# Configure npm to use it
|
||||
npm config set prefix "$HOME/.npm-global"
|
||||
|
||||
# Add to PATH for current session
|
||||
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||
|
||||
# Add to shell profile for future sessions
|
||||
if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" ~/.bashrc; then
|
||||
echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> ~/.bashrc
|
||||
fi
|
||||
@@ -108,7 +97,6 @@ function append_extensions_to_settings_json() {
|
||||
fi
|
||||
if [ ! -f "$SETTINGS_PATH" ]; then
|
||||
printf "%s does not exist. Creating with merged mcpServers structure.\n" "$SETTINGS_PATH"
|
||||
# If ADDITIONAL_EXTENSIONS is not set or empty, use '{}'
|
||||
ADD_EXT_JSON='{}'
|
||||
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
|
||||
ADD_EXT_JSON="$ADDITIONAL_EXTENSIONS"
|
||||
@@ -116,10 +104,7 @@ function append_extensions_to_settings_json() {
|
||||
printf '{"mcpServers":%s}\n' "$(jq -s 'add' <(echo "$BASE_EXTENSIONS") <(echo "$ADD_EXT_JSON"))" > "$SETTINGS_PATH"
|
||||
fi
|
||||
|
||||
# Prepare temp files
|
||||
TMP_SETTINGS=$(mktemp)
|
||||
|
||||
# If ADDITIONAL_EXTENSIONS is not set or empty, use '{}'
|
||||
ADD_EXT_JSON='{}'
|
||||
if [ -n "${ADDITIONAL_EXTENSIONS:-}" ]; then
|
||||
printf "[append_extensions_to_settings_json] ADDITIONAL_EXTENSIONS is set.\n"
|
||||
@@ -133,14 +118,13 @@ function append_extensions_to_settings_json() {
|
||||
'.mcpServers = (.mcpServers // {} + $base + $add)' \
|
||||
"$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
|
||||
# Add theme and selectedAuthType fields
|
||||
jq '.theme = "Default" | .selectedAuthType = "gemini-api-key"' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH"
|
||||
|
||||
printf "[append_extensions_to_settings_json] Merge complete.\n"
|
||||
}
|
||||
|
||||
function add_instruction_prompt_if_exists() {
|
||||
if [ -n "${GEMINI_INSTRUCTION_PROMPT:-}" ]; then
|
||||
function add_system_prompt_if_exists() {
|
||||
if [ -n "${GEMINI_SYSTEM_PROMPT:-}" ]; then
|
||||
if [ -d "${GEMINI_START_DIRECTORY}" ]; then
|
||||
printf "Directory '%s' exists. Changing to it.\\n" "${GEMINI_START_DIRECTORY}"
|
||||
cd "${GEMINI_START_DIRECTORY}" || {
|
||||
@@ -160,16 +144,21 @@ function add_instruction_prompt_if_exists() {
|
||||
fi
|
||||
touch GEMINI.md
|
||||
printf "Setting GEMINI.md\n"
|
||||
echo "${GEMINI_INSTRUCTION_PROMPT}" > GEMINI.md
|
||||
echo "${GEMINI_SYSTEM_PROMPT}" > GEMINI.md
|
||||
else
|
||||
printf "GEMINI.md is not set.\n"
|
||||
fi
|
||||
}
|
||||
|
||||
function configure_mcp() {
|
||||
export CODER_MCP_APP_STATUS_SLUG="gemini"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure gemini "${GEMINI_START_DIRECTORY}"
|
||||
}
|
||||
|
||||
# Install Gemini
|
||||
install_gemini
|
||||
gemini --version
|
||||
populate_settings_json
|
||||
add_instruction_prompt_if_exists
|
||||
add_system_prompt_if_exists
|
||||
configure_mcp
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
# Load shell environment
|
||||
source "$HOME"/.bashrc
|
||||
|
||||
command_exists() {
|
||||
@@ -15,7 +16,8 @@ fi
|
||||
|
||||
printf "Version: %s\n" "$(gemini --version)"
|
||||
|
||||
GEMINI_TASK_PROMPT=$(echo -n "$GEMINI_TASK_PROMPT" | base64 -d)
|
||||
MODULE_DIR="$HOME/.gemini-module"
|
||||
mkdir -p "$MODULE_DIR"
|
||||
|
||||
if command_exists gemini; then
|
||||
printf "Gemini is installed\n"
|
||||
@@ -43,20 +45,30 @@ else
|
||||
fi
|
||||
|
||||
if [ -n "$GEMINI_TASK_PROMPT" ]; then
|
||||
printf "Running the task prompt %s\n" "$GEMINI_TASK_PROMPT"
|
||||
printf "Running automated task: %s\n" "$GEMINI_TASK_PROMPT"
|
||||
PROMPT="Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GEMINI_TASK_PROMPT"
|
||||
PROMPT_FILE="$MODULE_DIR/prompt.txt"
|
||||
echo -n "$PROMPT" >"$PROMPT_FILE"
|
||||
GEMINI_ARGS=(--prompt-interactive "$PROMPT")
|
||||
else
|
||||
printf "No task prompt given.\n"
|
||||
printf "Starting Gemini CLI in interactive mode.\n"
|
||||
GEMINI_ARGS=()
|
||||
fi
|
||||
|
||||
if [ -n "$GEMINI_API_KEY" ]; then
|
||||
printf "gemini_api_key provided !\n"
|
||||
else
|
||||
printf "gemini_api_key not provided\n"
|
||||
if [ -n "$GEMINI_YOLO_MODE" ] && [ "$GEMINI_YOLO_MODE" = "true" ]; then
|
||||
printf "YOLO mode enabled - will auto-approve all tool calls\n"
|
||||
GEMINI_ARGS+=(--yolo)
|
||||
fi
|
||||
|
||||
# use low width to fit in the tasks UI sidebar. height is adjusted so that width x height ~= 80x1000 characters
|
||||
# are visible in the terminal screen by default.
|
||||
agentapi server --term-width 67 --term-height 1190 -- gemini "${GEMINI_ARGS[@]}"
|
||||
if [ -n "$GEMINI_API_KEY" ] || [ -n "$GOOGLE_API_KEY" ]; then
|
||||
if [ -n "$GOOGLE_GENAI_USE_VERTEXAI" ] && [ "$GOOGLE_GENAI_USE_VERTEXAI" = "true" ]; then
|
||||
printf "Using Vertex AI with API key\n"
|
||||
else
|
||||
printf "Using direct Gemini API with API key\n"
|
||||
fi
|
||||
else
|
||||
printf "No API key provided (neither GEMINI_API_KEY nor GOOGLE_API_KEY)\n"
|
||||
fi
|
||||
|
||||
agentapi server --term-width 67 --term-height 1190 -- \
|
||||
bash -c "$(printf '%q ' gemini "${GEMINI_ARGS[@]}")"
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$1" == "--version" ]]; then
|
||||
echo "HELLO: $(bash -c env)"
|
||||
echo "gemini version v2.5.0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
while true; do
|
||||
echo "$(date) - gemini-mock"
|
||||
sleep 15
|
||||
done
|
||||
@@ -0,0 +1,50 @@
|
||||
run "required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
run "offline_and_use_cached_conflict" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
use_cached = true
|
||||
offline = true
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.code-server
|
||||
]
|
||||
}
|
||||
|
||||
run "offline_disallows_extensions" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
offline = true
|
||||
extensions = ["ms-python.python", "golang.go"]
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.code-server
|
||||
]
|
||||
}
|
||||
|
||||
run "url_with_folder_query" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder/project"
|
||||
port = 13337
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.code-server.url == "http://localhost:13337/?folder=%2Fhome%2Fcoder%2Fproject"
|
||||
error_message = "coder_app URL must include encoded folder query param"
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -29,8 +29,33 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Configure MCP servers for Cursor
|
||||
|
||||
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.cursor/mcp.json` using a `coder_script` on workspace start.
|
||||
|
||||
```tf
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
coder = {
|
||||
command = "coder"
|
||||
args = ["exp", "mcp", "server"]
|
||||
env = {
|
||||
CODER_MCP_APP_STATUS_SLUG = "cursor"
|
||||
CODER_MCP_AI_AGENTAPI_URL = "http://localhost:3284"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
} from "~test";
|
||||
|
||||
describe("cursor", async () => {
|
||||
@@ -85,4 +90,26 @@ describe("cursor", async () => {
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.cursor/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const mcp = JSON.stringify({ servers: { demo: { url: "http://localhost:1234" } } });
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
mcp,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script", "cursor_mcp").script;
|
||||
const resp = await execContainer(id, ["sh", "-c", script]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(id, "/root/.cursor/mcp.json");
|
||||
expect(content).toBe(mcp);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,9 +50,20 @@ variable "display_name" {
|
||||
default = "Cursor Desktop"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Cursor. When set, writes ~/.cursor/mcp.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
@@ -75,6 +86,21 @@ resource "coder_app" "cursor" {
|
||||
])
|
||||
}
|
||||
|
||||
resource "coder_script" "cursor_mcp" {
|
||||
count = var.mcp != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Cursor MCP"
|
||||
icon = "/icon/cursor.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p "$HOME/.cursor"
|
||||
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.cursor/mcp.json"
|
||||
EOT
|
||||
}
|
||||
|
||||
output "cursor_url" {
|
||||
value = coder_app.cursor.url
|
||||
description = "Cursor IDE Desktop URL."
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@@ -139,7 +139,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -14,7 +14,7 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -39,7 +39,7 @@ When `default` contains IDE codes, those IDEs are created directly without user
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA
|
||||
@@ -52,7 +52,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
# Show parameter with limited options
|
||||
@@ -66,7 +66,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
default = ["IU", "PY"]
|
||||
@@ -81,7 +81,7 @@ module "jetbrains" {
|
||||
module "jetbrains" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
@@ -107,7 +107,7 @@ module "jetbrains" {
|
||||
module "jetbrains_pycharm" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jetbrains/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/workspace/project"
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
run "requires_agent_and_folder" {
|
||||
command = plan
|
||||
|
||||
# Setting both required vars should plan
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
}
|
||||
}
|
||||
|
||||
run "creates_parameter_when_default_empty_latest" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
major_version = "latest"
|
||||
}
|
||||
|
||||
# When default is empty, a coder_parameter should be created
|
||||
assert {
|
||||
condition = can(data.coder_parameter.jetbrains_ides[0].type)
|
||||
error_message = "Expected data.coder_parameter.jetbrains_ides to exist when default is empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "no_apps_when_default_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_app.jetbrains) == 0
|
||||
error_message = "Expected no coder_app resources when default is empty"
|
||||
}
|
||||
}
|
||||
|
||||
run "single_app_when_default_GO" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_app.jetbrains) == 1
|
||||
error_message = "Expected exactly one coder_app when default contains GO"
|
||||
}
|
||||
}
|
||||
|
||||
run "url_contains_required_params" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
folder = "/custom/project/path"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("jetbrains://gateway/coder", app.url)) > 0])
|
||||
error_message = "URL must contain jetbrains scheme"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&folder=/custom/project/path", app.url)) > 0])
|
||||
error_message = "URL must include folder path"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_product_code=GO", app.url)) > 0])
|
||||
error_message = "URL must include product code"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("ide_build_number=", app.url)) > 0])
|
||||
error_message = "URL must include build number"
|
||||
}
|
||||
}
|
||||
|
||||
run "includes_agent_name_when_set" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
agent_name = "main-agent"
|
||||
folder = "/custom/project/path"
|
||||
default = ["GO"]
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : length(regexall("&agent_name=main-agent", app.url)) > 0])
|
||||
error_message = "URL must include agent_name when provided"
|
||||
}
|
||||
}
|
||||
|
||||
run "parameter_order_when_default_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
coder_parameter_order = 5
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = data.coder_parameter.jetbrains_ides[0].order == 5
|
||||
error_message = "Expected coder_parameter order to be set to 5"
|
||||
}
|
||||
}
|
||||
|
||||
run "app_order_when_default_not_empty" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/home/coder"
|
||||
default = ["GO"]
|
||||
coder_app_order = 10
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = anytrue([for app in values(resource.coder_app.jetbrains) : app.order == 10])
|
||||
error_message = "Expected coder_app order to be set to 10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
display_name: VSCode Desktop Core
|
||||
description: Building block for modules that need to link to an external VSCode-based IDE
|
||||
icon: ../../../../.icons/coder.svg
|
||||
verified: true
|
||||
tags: [internal, library]
|
||||
---
|
||||
|
||||
# VS Code Desktop Core
|
||||
|
||||
> [!CAUTION]
|
||||
> We do not recommend using this module directly. Instead, please consider using one of our [Desktop IDE modules](https://registry.coder.com/modules?search=tag%3Aide).
|
||||
|
||||
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily to be used as a library to create modules for VSCode-based IDEs.
|
||||
|
||||
```tf
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/code.svg"
|
||||
coder_app_slug = "vscode"
|
||||
coder_app_display_name = "VS Code Desktop"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "vscode"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
// hardcoded coder_app name in main.tf
|
||||
const appName = "vscode-desktop";
|
||||
|
||||
const defaultVariables = {
|
||||
agent_id: "foo",
|
||||
coder_app_icon: "/icon/code.svg",
|
||||
coder_app_slug: "vscode",
|
||||
coder_app_display_name: "VS Code Desktop",
|
||||
protocol: "vscode",
|
||||
}
|
||||
|
||||
describe("vscode-desktop-core", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, defaultVariables);
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, defaultVariables);
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
|
||||
...defaultVariables
|
||||
});
|
||||
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_app_order: "22",
|
||||
...defaultVariables
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in the IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "protocol" {
|
||||
type = string
|
||||
description = "The URI protocol for the IDE."
|
||||
}
|
||||
|
||||
variable "coder_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "vscode-desktop" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
|
||||
icon = var.coder_app_icon
|
||||
slug = var.coder_app_slug
|
||||
display_name = var.coder_app_display_name
|
||||
|
||||
order = var.coder_app_order
|
||||
group = var.coder_app_group
|
||||
|
||||
# While the call to "join" is not strictly necessary, it makes the URL more readable.
|
||||
url = join("", [
|
||||
"${var.protocol}://coder.coder-remote/open",
|
||||
"?owner=${data.coder_workspace_owner.me.name}",
|
||||
"&workspace=${data.coder_workspace.me.name}",
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=${data.coder_workspace.me.access_url}",
|
||||
# NOTE: There is a protocol whitelist for the token replacement, so this will only work with the protocols hardcoded in the front-end.
|
||||
# (https://github.com/coder/coder/blob/6ba4b5bbc95e2e528d7f5b1e31fffa200ae1a6db/site/src/modules/apps/apps.ts#L18)
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
}
|
||||
|
||||
output "ide_uri" {
|
||||
value = coder_app.vscode-desktop.url
|
||||
description = "IDE URI."
|
||||
}
|
||||
@@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
@@ -32,7 +32,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -44,7 +44,7 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
display_name = "Zed Editor"
|
||||
order = 1
|
||||
@@ -57,8 +57,36 @@ module "zed" {
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = coder_agent.example.name
|
||||
}
|
||||
```
|
||||
|
||||
### Configure Zed settings including MCP servers
|
||||
|
||||
Zed stores settings at `~/.config/zed/settings.json` by default. If `XDG_CONFIG_HOME` is set on Linux, settings will be at `$XDG_CONFIG_HOME/zed/settings.json`.
|
||||
|
||||
You can declaratively set/merge settings with the `settings` input. Provide a JSON string (e.g., via `jsonencode(...)`). For example, to configure MCP servers:
|
||||
|
||||
```tf
|
||||
module "zed" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/zed/coder"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
settings = jsonencode({
|
||||
context_servers = {
|
||||
your-mcp-server = {
|
||||
source = "custom"
|
||||
command = "some-command"
|
||||
args = ["arg-1", "arg-2"]
|
||||
env = {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
See Zed’s settings files documentation: https://zed.dev/docs/configuring-zed#settings-files
|
||||
|
||||
@@ -50,7 +50,14 @@ variable "display_name" {
|
||||
default = "Zed"
|
||||
}
|
||||
|
||||
variable "settings" {
|
||||
type = string
|
||||
description = "JSON encoded settings.json"
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
@@ -60,6 +67,30 @@ locals {
|
||||
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
|
||||
}
|
||||
|
||||
resource "coder_script" "zed_settings" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Configure Zed settings"
|
||||
icon = "/icon/zed.svg"
|
||||
run_on_start = true
|
||||
script = <<-EOT
|
||||
set -eu
|
||||
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
|
||||
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
CONFIG_HOME="$${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
ZED_DIR="$${CONFIG_HOME}/zed"
|
||||
mkdir -p "$${ZED_DIR}"
|
||||
SETTINGS_FILE="$${ZED_DIR}/settings.json"
|
||||
if command -v jq >/dev/null 2>&1 && [ -s "$${SETTINGS_FILE}" ]; then
|
||||
tmpfile="$(mktemp)"
|
||||
jq -s '.[0] * .[1]' "$${SETTINGS_FILE}" <(printf '%s\n' "$${SETTINGS_JSON}") > "$${tmpfile}" && mv "$${tmpfile}" "$${SETTINGS_FILE}"
|
||||
else
|
||||
printf '%s\n' "$${SETTINGS_JSON}" > "$${SETTINGS_FILE}"
|
||||
fi
|
||||
EOT
|
||||
}
|
||||
|
||||
resource "coder_app" "zed" {
|
||||
agent_id = var.agent_id
|
||||
display_name = var.display_name
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
run "default_output" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/default.coder"
|
||||
error_message = "zed_url did not match expected default URL"
|
||||
}
|
||||
}
|
||||
|
||||
run "adds_folder" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
folder = "/foo/bar"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/default.coder/foo/bar"
|
||||
error_message = "zed_url did not include provided folder path"
|
||||
}
|
||||
}
|
||||
|
||||
run "adds_agent_name" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
agent_name = "myagent"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.zed_url == "zed://ssh/myagent.default.default.coder"
|
||||
error_message = "zed_url did not include agent_name in hostname"
|
||||
}
|
||||
}
|
||||
+486
-97
@@ -2,32 +2,221 @@
|
||||
|
||||
# Tag Release Script
|
||||
# Automatically detects modules that need tagging and creates release tags
|
||||
# Usage: ./tag_release.sh
|
||||
# Usage: ./tag_release.sh [OPTIONS]
|
||||
# Operates on the current checked-out commit
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODULES_TO_TAG=()
|
||||
AUTO_APPROVE=false
|
||||
DRY_RUN=false
|
||||
VERBOSE=false
|
||||
QUIET=false
|
||||
OUTPUT_FORMAT="plain"
|
||||
TARGET_NAMESPACE=""
|
||||
TARGET_MODULE=""
|
||||
SKIP_PUSH=false
|
||||
|
||||
JSON_OUTPUT='{
|
||||
"metadata": {},
|
||||
"summary": {},
|
||||
"modules": [],
|
||||
"warnings": [],
|
||||
"errors": []
|
||||
}'
|
||||
|
||||
readonly EXIT_SUCCESS=0
|
||||
readonly EXIT_ERROR=1
|
||||
readonly EXIT_NO_ACTION_NEEDED=2
|
||||
readonly EXIT_VALIDATION_FAILED=3
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0"
|
||||
echo ""
|
||||
echo "This script will:"
|
||||
echo " 1. Scan all modules in the registry"
|
||||
echo " 2. Check which modules need new release tags"
|
||||
echo " 3. Extract version information from README files"
|
||||
echo " 4. Generate a report for confirmation"
|
||||
echo " 5. Create and push release tags after confirmation"
|
||||
echo ""
|
||||
echo "The script operates on the current checked-out commit."
|
||||
echo "Make sure you have checked out the commit you want to tag before running."
|
||||
exit 1
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-y, --auto-approve Skip confirmation prompt
|
||||
-d, --dry-run Preview without creating tags
|
||||
-v, --verbose Detailed output
|
||||
-q, --quiet Minimal output
|
||||
-f, --format=FORMAT Output format: 'plain' or 'json'
|
||||
-n, --namespace=NAME Target specific namespace
|
||||
-m, --module=NAME Target specific module
|
||||
-s, --skip-push Create tags but don't push
|
||||
-h, --help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
$0 # Interactive mode
|
||||
$0 -y -q -f json # CI/CD automation
|
||||
$0 -d -v # Test with verbose output
|
||||
$0 -m code-server -d # Target specific module
|
||||
$0 -n coder -m code-server -d # Target module in namespace
|
||||
|
||||
Exit codes: 0=success, 1=error, 2=no action needed, 3=validation failed
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message="$*"
|
||||
local timestamp
|
||||
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
case "$level" in
|
||||
"ERROR")
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
add_json_error "script_error" "$message"
|
||||
elif [[ "$QUIET" != "true" ]]; then
|
||||
echo "❌ $message" >&2
|
||||
fi
|
||||
;;
|
||||
"WARN")
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
add_json_warning "" "$message" "warning"
|
||||
elif [[ "$QUIET" != "true" ]]; then
|
||||
echo "⚠️ $message" >&2
|
||||
fi
|
||||
;;
|
||||
"INFO")
|
||||
if [[ "$QUIET" != "true" && "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo "$message"
|
||||
fi
|
||||
;;
|
||||
"SUCCESS")
|
||||
if [[ "$QUIET" != "true" && "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo "✅ $message"
|
||||
fi
|
||||
;;
|
||||
"DEBUG")
|
||||
if [[ "$VERBOSE" == "true" && "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo "🔍 [$timestamp] $message" >&2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
add_json_error() {
|
||||
local type="$1"
|
||||
local message="$2"
|
||||
local details="${3:-}"
|
||||
local exit_code="${4:-1}"
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg type "$type" --arg msg "$message" --arg details "$details" --argjson code "$exit_code" \
|
||||
'.errors += [{"type": $type, "message": $msg, "details": $details, "exit_code": $code}]')
|
||||
}
|
||||
|
||||
add_json_warning() {
|
||||
local module="$1"
|
||||
local message="$2"
|
||||
local type="$3"
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg module "$module" --arg msg "$message" --arg type "$type" \
|
||||
'.warnings += [{"module": $module, "message": $msg, "type": $type}]')
|
||||
}
|
||||
|
||||
add_json_module() {
|
||||
local namespace="$1"
|
||||
local module_name="$2"
|
||||
local path="$3"
|
||||
local version="$4"
|
||||
local tag_name="$5"
|
||||
local status="$6"
|
||||
local already_existed="$7"
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg ns "$namespace" --arg name "$module_name" --arg path "$path" \
|
||||
--arg version "$version" --arg tag "$tag_name" --arg status "$status" --argjson existed "$already_existed" \
|
||||
'.modules += [{"namespace": $ns, "module_name": $name, "path": $path, "version": $version, "tag_name": $tag, "status": $status, "already_existed": $existed}]')
|
||||
}
|
||||
|
||||
parse_arguments() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-y | --auto-approve)
|
||||
AUTO_APPROVE=true
|
||||
shift
|
||||
;;
|
||||
-d | --dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-v | --verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
-q | --quiet)
|
||||
QUIET=true
|
||||
shift
|
||||
;;
|
||||
-f | --format=* | --format)
|
||||
if [[ "$1" == "-f" || "$1" == "--format" ]]; then
|
||||
if [[ -z "$2" ]]; then
|
||||
log "ERROR" "Option $1 requires a value"
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
OUTPUT_FORMAT="$2"
|
||||
shift 2
|
||||
else
|
||||
OUTPUT_FORMAT="${1#*=}"
|
||||
shift
|
||||
fi
|
||||
if [[ "$OUTPUT_FORMAT" != "plain" && "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
log "ERROR" "Invalid format '$OUTPUT_FORMAT'. Must be 'plain' or 'json'"
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
;;
|
||||
-n | --namespace=* | --namespace)
|
||||
if [[ "$1" == "-n" || "$1" == "--namespace" ]]; then
|
||||
if [[ -z "$2" ]]; then
|
||||
log "ERROR" "Option $1 requires a value"
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
TARGET_NAMESPACE="$2"
|
||||
shift 2
|
||||
else
|
||||
TARGET_NAMESPACE="${1#*=}"
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
-m | --module=* | --module)
|
||||
if [[ "$1" == "-m" || "$1" == "--module" ]]; then
|
||||
if [[ -z "$2" ]]; then
|
||||
log "ERROR" "Option $1 requires a value"
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
TARGET_MODULE="$2"
|
||||
shift 2
|
||||
else
|
||||
TARGET_MODULE="${1#*=}"
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
-s | --skip-push)
|
||||
SKIP_PUSH=true
|
||||
shift
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log "ERROR" "Unknown option: $1"
|
||||
echo "Use --help for usage information."
|
||||
exit $EXIT_ERROR
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$VERBOSE" == "true" && "$QUIET" == "true" ]]; then
|
||||
echo "❌ --verbose and --quiet cannot be used together" >&2
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
}
|
||||
|
||||
validate_version() {
|
||||
local version="$1"
|
||||
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ Invalid version format: '$version'. Expected X.Y.Z format." >&2
|
||||
log "DEBUG" "Invalid version format: '$version'. Expected X.Y.Z format."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
@@ -38,7 +227,12 @@ extract_version_from_readme() {
|
||||
local namespace="$2"
|
||||
local module_name="$3"
|
||||
|
||||
[ ! -f "$readme_path" ] && return 1
|
||||
log "DEBUG" "Extracting version from $readme_path for $namespace/$module_name"
|
||||
|
||||
[ ! -f "$readme_path" ] && {
|
||||
log "DEBUG" "README file not found: $readme_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
local version_line
|
||||
version_line=$(grep -E "source\s*=\s*\"registry\.coder\.com/${namespace}/${module_name}" "$readme_path" | head -1 || echo "")
|
||||
@@ -47,6 +241,7 @@ extract_version_from_readme() {
|
||||
local version
|
||||
version=$(echo "$version_line" | sed -n 's/.*version\s*=\s*"\([^"]*\)".*/\1/p')
|
||||
if [ -n "$version" ]; then
|
||||
log "DEBUG" "Found version '$version' from source line: $version_line"
|
||||
echo "$version"
|
||||
return 0
|
||||
fi
|
||||
@@ -56,10 +251,12 @@ extract_version_from_readme() {
|
||||
fallback_version=$(grep -E 'version\s*=\s*"[0-9]+\.[0-9]+\.[0-9]+"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/' || echo "")
|
||||
|
||||
if [ -n "$fallback_version" ]; then
|
||||
log "DEBUG" "Found fallback version '$fallback_version'"
|
||||
echo "$fallback_version"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "DEBUG" "No version found in $readme_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -70,29 +267,54 @@ check_module_needs_tagging() {
|
||||
|
||||
local tag_name="release/${namespace}/${module_name}/v${readme_version}"
|
||||
|
||||
log "DEBUG" "Checking if tag exists: $tag_name"
|
||||
|
||||
if git rev-parse --verify "$tag_name" > /dev/null 2>&1; then
|
||||
log "DEBUG" "Tag $tag_name already exists"
|
||||
return 1
|
||||
else
|
||||
log "DEBUG" "Tag $tag_name needs to be created"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
should_process_module() {
|
||||
local namespace="$1"
|
||||
local module_name="$2"
|
||||
|
||||
if [[ -n "$TARGET_NAMESPACE" && "$TARGET_NAMESPACE" != "$namespace" ]]; then
|
||||
log "DEBUG" "Skipping $namespace/$module_name: namespace filter"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$TARGET_MODULE" && "$TARGET_MODULE" != "$module_name" ]]; then
|
||||
log "DEBUG" "Skipping $namespace/$module_name: module filter"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
detect_modules_needing_tags() {
|
||||
MODULES_TO_TAG=()
|
||||
|
||||
echo "🔍 Scanning all modules for missing release tags..."
|
||||
echo ""
|
||||
log "INFO" "🔍 Scanning all modules for missing release tags..."
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
local all_modules
|
||||
all_modules=$(find registry -mindepth 3 -maxdepth 3 -type d -path "*/modules/*" | sort -u || echo "")
|
||||
|
||||
[ -z "$all_modules" ] && {
|
||||
echo "❌ No modules found to check"
|
||||
return 1
|
||||
log "ERROR" "No modules found to check"
|
||||
return $EXIT_ERROR
|
||||
}
|
||||
|
||||
local total_checked=0
|
||||
local needs_tagging=0
|
||||
local already_tagged=0
|
||||
local skipped=0
|
||||
|
||||
while IFS= read -r module_path; do
|
||||
if [ -z "$module_path" ]; then continue; fi
|
||||
@@ -102,64 +324,133 @@ detect_modules_needing_tags() {
|
||||
local module_name
|
||||
module_name=$(echo "$module_path" | cut -d'/' -f4)
|
||||
|
||||
if ! should_process_module "$namespace" "$module_name"; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
total_checked=$((total_checked + 1))
|
||||
|
||||
local readme_path="$module_path/README.md"
|
||||
local readme_version
|
||||
|
||||
if ! readme_version=$(extract_version_from_readme "$readme_path" "$namespace" "$module_name"); then
|
||||
echo "⚠️ $namespace/$module_name: No version found in README, skipping"
|
||||
log "WARN" "$namespace/$module_name: No version found in README, skipping"
|
||||
add_json_warning "$namespace/$module_name" "No version found in README, skipping" "missing_version"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! validate_version "$readme_version"; then
|
||||
echo "⚠️ $namespace/$module_name: Invalid version format '$readme_version', skipping"
|
||||
log "WARN" "$namespace/$module_name: Invalid version format '$readme_version', skipping"
|
||||
add_json_warning "$namespace/$module_name" "Invalid version format '$readme_version', skipping" "invalid_version"
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
local tag_name="release/$namespace/$module_name/v$readme_version"
|
||||
|
||||
if check_module_needs_tagging "$namespace" "$module_name" "$readme_version"; then
|
||||
echo "📦 $namespace/$module_name: v$readme_version (needs tag)"
|
||||
log "INFO" "📦 $namespace/$module_name: v$readme_version (needs tag)"
|
||||
MODULES_TO_TAG+=("$module_path:$namespace:$module_name:$readme_version")
|
||||
needs_tagging=$((needs_tagging + 1))
|
||||
|
||||
local status="needs_tagging"
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
status="would_be_tagged"
|
||||
fi
|
||||
add_json_module "$namespace" "$module_name" "$module_path" "$readme_version" "$tag_name" "$status" false
|
||||
else
|
||||
echo "✅ $namespace/$module_name: v$readme_version (already tagged)"
|
||||
log "SUCCESS" "$namespace/$module_name: v$readme_version (already tagged)"
|
||||
already_tagged=$((already_tagged + 1))
|
||||
add_json_module "$namespace" "$module_name" "$module_path" "$readme_version" "$tag_name" "already_tagged" true
|
||||
fi
|
||||
|
||||
done <<< "$all_modules"
|
||||
|
||||
echo ""
|
||||
echo "📊 Summary: $needs_tagging of $total_checked modules need tagging"
|
||||
echo ""
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --argjson total "$total_checked" --argjson needs "$needs_tagging" \
|
||||
--argjson tagged "$already_tagged" --argjson skip "$skipped" \
|
||||
'.summary.total_scanned = $total | .summary.needs_tagging = $needs | .summary.already_tagged = $tagged | .summary.skipped = $skip')
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo ""
|
||||
log "INFO" "📊 Summary: $needs_tagging of $total_checked modules need tagging"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
[ $needs_tagging -eq 0 ] && {
|
||||
echo "🎉 All modules are up to date! No tags needed."
|
||||
return 0
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
log "SUCCESS" "🎉 All modules are up to date! No tags needed."
|
||||
fi
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "no_action_needed"')
|
||||
return $EXIT_NO_ACTION_NEEDED
|
||||
}
|
||||
|
||||
echo "## Tags to be created:"
|
||||
for module_info in "${MODULES_TO_TAG[@]}"; do
|
||||
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
|
||||
echo "- \`release/$namespace/$module_name/v$version\`"
|
||||
done
|
||||
echo ""
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo "## Tags to be created:"
|
||||
for module_info in "${MODULES_TO_TAG[@]}"; do
|
||||
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
|
||||
echo "- \`release/$namespace/$module_name/v$version\`"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
return 0
|
||||
return $EXIT_SUCCESS
|
||||
}
|
||||
|
||||
pre_flight_checks() {
|
||||
log "DEBUG" "Running pre-flight checks..."
|
||||
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
log "ERROR" "Not in a git repository"
|
||||
return $EXIT_ERROR
|
||||
fi
|
||||
|
||||
if ! git remote get-url origin > /dev/null 2>&1; then
|
||||
log "ERROR" "No 'origin' remote found"
|
||||
return $EXIT_ERROR
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_PUSH" != "true" && "$DRY_RUN" != "true" ]]; then
|
||||
log "DEBUG" "Testing remote connectivity..."
|
||||
if ! git ls-remote --exit-code origin > /dev/null 2>&1; then
|
||||
log "ERROR" "Cannot connect to remote repository"
|
||||
return $EXIT_ERROR
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! git rev-parse HEAD > /dev/null 2>&1; then
|
||||
log "ERROR" "Cannot determine current commit"
|
||||
return $EXIT_ERROR
|
||||
fi
|
||||
|
||||
log "DEBUG" "Pre-flight checks passed"
|
||||
return $EXIT_SUCCESS
|
||||
}
|
||||
|
||||
create_and_push_tags() {
|
||||
[ ${#MODULES_TO_TAG[@]} -eq 0 ] && {
|
||||
echo "❌ No modules to tag found"
|
||||
return 1
|
||||
log "ERROR" "No modules to tag found"
|
||||
return $EXIT_ERROR
|
||||
}
|
||||
|
||||
local current_commit
|
||||
current_commit=$(git rev-parse HEAD)
|
||||
|
||||
echo "🏷️ Creating release tags for commit: $current_commit"
|
||||
echo ""
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log "INFO" "🏷️ [DRY RUN] Would create release tags for commit: $current_commit"
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "dry_run" | .summary.tags_created = 0 | .summary.tags_pushed = 0')
|
||||
return $EXIT_SUCCESS
|
||||
fi
|
||||
|
||||
log "INFO" "🏷️ Creating release tags for commit: $current_commit"
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo ""
|
||||
fi
|
||||
|
||||
local created_tags=0
|
||||
local failed_tags=0
|
||||
local created_tag_names=()
|
||||
|
||||
for module_info in "${MODULES_TO_TAG[@]}"; do
|
||||
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
|
||||
@@ -167,35 +458,56 @@ create_and_push_tags() {
|
||||
local tag_name="release/$namespace/$module_name/v$version"
|
||||
local tag_message="Release $namespace/$module_name v$version"
|
||||
|
||||
echo "Creating tag: $tag_name"
|
||||
log "DEBUG" "Creating tag: $tag_name"
|
||||
log "INFO" "Creating tag: $tag_name"
|
||||
|
||||
if git tag -a "$tag_name" -m "$tag_message" "$current_commit"; then
|
||||
echo "✅ Created: $tag_name"
|
||||
if git tag -a "$tag_name" -m "$tag_message" "$current_commit" 2> /dev/null; then
|
||||
log "SUCCESS" "Created: $tag_name"
|
||||
created_tags=$((created_tags + 1))
|
||||
created_tag_names+=("$tag_name")
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg tag "$tag_name" \
|
||||
'(.modules[] | select(.tag_name == $tag) | .status) = "tag_created"')
|
||||
else
|
||||
echo "❌ Failed to create: $tag_name"
|
||||
log "ERROR" "Failed to create: $tag_name"
|
||||
add_json_error "tag_creation_failed" "Failed to create tag: $tag_name" "git tag -a $tag_name -m '$tag_message' $current_commit"
|
||||
failed_tags=$((failed_tags + 1))
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg tag "$tag_name" \
|
||||
'(.modules[] | select(.tag_name == $tag) | .status) = "tag_creation_failed"')
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "📊 Tag creation summary:"
|
||||
echo " Created: $created_tags"
|
||||
echo " Failed: $failed_tags"
|
||||
echo ""
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo ""
|
||||
log "INFO" "📊 Tag creation summary:"
|
||||
log "INFO" " Created: $created_tags"
|
||||
log "INFO" " Failed: $failed_tags"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
[ $created_tags -eq 0 ] && {
|
||||
echo "❌ No tags were created successfully"
|
||||
return 1
|
||||
log "ERROR" "No tags were created successfully"
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "failed" | .summary.tags_created = 0 | .summary.tags_pushed = 0')
|
||||
return $EXIT_ERROR
|
||||
}
|
||||
|
||||
echo "🚀 Pushing tags to origin..."
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --argjson created "$created_tags" '.summary.tags_created = $created')
|
||||
|
||||
if [[ "$SKIP_PUSH" == "true" ]]; then
|
||||
log "INFO" "🚫 Skipping push (--skip-push specified)"
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "tags_created_not_pushed" | .summary.tags_pushed = 0')
|
||||
for tag_name in "${created_tag_names[@]}"; do
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg tag "$tag_name" \
|
||||
'(.modules[] | select(.tag_name == $tag) | .status) = "tag_created_not_pushed"')
|
||||
done
|
||||
return $EXIT_SUCCESS
|
||||
fi
|
||||
|
||||
log "INFO" "🚀 Pushing tags to origin..."
|
||||
|
||||
local tags_to_push=()
|
||||
for module_info in "${MODULES_TO_TAG[@]}"; do
|
||||
IFS=':' read -r module_path namespace module_name version <<< "$module_info"
|
||||
local tag_name="release/$namespace/$module_name/v$version"
|
||||
|
||||
for tag_name in "${created_tag_names[@]}"; do
|
||||
if git rev-parse --verify "$tag_name" > /dev/null 2>&1; then
|
||||
tags_to_push+=("$tag_name")
|
||||
fi
|
||||
@@ -205,71 +517,148 @@ create_and_push_tags() {
|
||||
local failed_pushes=0
|
||||
|
||||
if [ ${#tags_to_push[@]} -eq 0 ]; then
|
||||
echo "❌ No valid tags found to push"
|
||||
log "ERROR" "No valid tags found to push"
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "failed" | .summary.tags_pushed = 0')
|
||||
else
|
||||
if git push --atomic origin "${tags_to_push[@]}"; then
|
||||
echo "✅ Successfully pushed all ${#tags_to_push[@]} tags"
|
||||
if git push --atomic origin "${tags_to_push[@]}" 2> /dev/null; then
|
||||
log "SUCCESS" "Successfully pushed all ${#tags_to_push[@]} tags"
|
||||
pushed_tags=${#tags_to_push[@]}
|
||||
|
||||
for tag_name in "${tags_to_push[@]}"; do
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg tag "$tag_name" \
|
||||
'(.modules[] | select(.tag_name == $tag) | .status) = "tagged_and_pushed"')
|
||||
done
|
||||
else
|
||||
echo "❌ Failed to push tags"
|
||||
log "ERROR" "Failed to push tags"
|
||||
add_json_error "push_failed" "Failed to push tags to remote" "git push --atomic origin ${tags_to_push[*]}"
|
||||
failed_pushes=${#tags_to_push[@]}
|
||||
|
||||
for tag_name in "${tags_to_push[@]}"; do
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg tag "$tag_name" \
|
||||
'(.modules[] | select(.tag_name == $tag) | .status) = "tag_created_push_failed"')
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📊 Push summary:"
|
||||
echo " Pushed: $pushed_tags"
|
||||
echo " Failed: $failed_pushes"
|
||||
echo ""
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --argjson pushed "$pushed_tags" '.summary.tags_pushed = $pushed')
|
||||
|
||||
if [ $pushed_tags -gt 0 ]; then
|
||||
echo "🎉 Successfully created and pushed $pushed_tags release tags!"
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
echo ""
|
||||
log "INFO" "📊 Push summary:"
|
||||
log "INFO" " Pushed: $pushed_tags"
|
||||
log "INFO" " Failed: $failed_pushes"
|
||||
echo ""
|
||||
echo "📝 Next steps:"
|
||||
echo " - Tags will be automatically published to registry.coder.com"
|
||||
echo " - Monitor the registry website for updates"
|
||||
echo " - Check GitHub releases for any issues"
|
||||
fi
|
||||
|
||||
return 0
|
||||
if [ $pushed_tags -gt 0 ]; then
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
log "SUCCESS" "🎉 Successfully created and pushed $pushed_tags release tags!"
|
||||
echo ""
|
||||
log "INFO" "📝 Next steps:"
|
||||
log "INFO" " - Tags will be automatically published to registry.coder.com"
|
||||
log "INFO" " - Monitor the registry website for updates"
|
||||
log "INFO" " - Check GitHub releases for any issues"
|
||||
fi
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "success"')
|
||||
return $EXIT_SUCCESS
|
||||
else
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "failed"')
|
||||
return $EXIT_ERROR
|
||||
fi
|
||||
}
|
||||
|
||||
finalize_json_output() {
|
||||
local timestamp
|
||||
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
local current_commit
|
||||
current_commit=$(git rev-parse HEAD 2> /dev/null || echo "unknown")
|
||||
local command_line="$0 $*"
|
||||
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq --arg ts "$timestamp" --arg commit "$current_commit" \
|
||||
--arg cmd "$command_line" \
|
||||
'.metadata.timestamp = $ts | .metadata.commit = $commit | .metadata.command = $cmd')
|
||||
|
||||
echo "$JSON_OUTPUT"
|
||||
}
|
||||
|
||||
main() {
|
||||
[ $# -gt 0 ] && usage
|
||||
parse_arguments "$@"
|
||||
|
||||
echo "🚀 Coder Registry Tag Release Script"
|
||||
echo "Operating on commit: $(git rev-parse HEAD)"
|
||||
echo ""
|
||||
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo "❌ Not in a git repository"
|
||||
exit 1
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
if ! command -v jq > /dev/null 2>&1; then
|
||||
echo '{"error": "jq is required for JSON output format but not found"}' >&2
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
fi
|
||||
|
||||
detect_modules_needing_tags || exit 1
|
||||
if [[ "$OUTPUT_FORMAT" != "json" ]]; then
|
||||
log "INFO" "🚀 Coder Registry Tag Release Script"
|
||||
log "INFO" "Operating on commit: $(git rev-parse HEAD 2> /dev/null || echo 'unknown')"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
[ ${#MODULES_TO_TAG[@]} -eq 0 ] && {
|
||||
echo "✨ No modules need tagging. All done!"
|
||||
exit 0
|
||||
}
|
||||
if ! pre_flight_checks; then
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "preflight_failed"')
|
||||
finalize_json_output "$@"
|
||||
fi
|
||||
exit $EXIT_ERROR
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "❓ Do you want to proceed with creating and pushing these release tags?"
|
||||
echo " This will create git tags and push them to the remote repository."
|
||||
echo ""
|
||||
read -p "Continue? [y/N]: " -r response
|
||||
local detect_exit_code
|
||||
detect_modules_needing_tags
|
||||
detect_exit_code=$?
|
||||
|
||||
case "$response" in
|
||||
[yY] | [yY][eE][sS])
|
||||
echo ""
|
||||
create_and_push_tags
|
||||
case $detect_exit_code in
|
||||
$EXIT_NO_ACTION_NEEDED)
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
finalize_json_output "$@"
|
||||
else
|
||||
log "SUCCESS" "✨ No modules need tagging. All done!"
|
||||
fi
|
||||
exit $EXIT_SUCCESS
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
echo "🚫 Operation cancelled by user"
|
||||
exit 0
|
||||
$EXIT_ERROR)
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "scan_failed"')
|
||||
finalize_json_output "$@"
|
||||
fi
|
||||
exit $EXIT_ERROR
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$AUTO_APPROVE" != "true" && "$OUTPUT_FORMAT" != "json" && "$DRY_RUN" != "true" ]]; then
|
||||
echo ""
|
||||
log "INFO" "❓ Do you want to proceed with creating and pushing these release tags?"
|
||||
log "INFO" " This will create git tags and push them to the remote repository."
|
||||
echo ""
|
||||
read -p "Continue? [y/N]: " -r response
|
||||
|
||||
case "$response" in
|
||||
[yY] | [yY][eE][sS])
|
||||
echo ""
|
||||
;;
|
||||
*)
|
||||
echo ""
|
||||
log "INFO" "🚫 Operation cancelled by user"
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
JSON_OUTPUT=$(echo "$JSON_OUTPUT" | jq '.summary.operation_status = "cancelled_by_user"')
|
||||
finalize_json_output "$@"
|
||||
fi
|
||||
exit $EXIT_SUCCESS
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
local create_exit_code
|
||||
create_and_push_tags
|
||||
create_exit_code=$?
|
||||
|
||||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||||
finalize_json_output "$@"
|
||||
fi
|
||||
|
||||
exit $create_exit_code
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Find all directories that contain any .tftest.hcl files and run terraform test in each
|
||||
|
||||
run_dir() {
|
||||
local dir="$1"
|
||||
echo "==> Running terraform test in $dir"
|
||||
(cd "$dir" && terraform init -upgrade -input=false -no-color > /dev/null && terraform test -no-color -verbose)
|
||||
}
|
||||
|
||||
mapfile -t test_dirs < <(find . -type f -name "*.tftest.hcl" -print0 | xargs -0 -I{} dirname {} | sort -u)
|
||||
|
||||
if [[ ${#test_dirs[@]} -eq 0 ]]; then
|
||||
echo "No .tftest.hcl tests found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
status=0
|
||||
for d in "${test_dirs[@]}"; do
|
||||
if ! run_dir "$d"; then
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit $status
|
||||
Reference in New Issue
Block a user