Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Smith 83a82345de wip: commit progress on terraform serialization 2025-05-21 19:25:43 +00:00
98 changed files with 870 additions and 1919 deletions
-18
View File
@@ -1,18 +0,0 @@
## Choose a PR Template
Please select the appropriate PR template for your contribution:
- 🆕 [New Module](?template=new_module.md) - Adding a new module to the registry
- 🐛 [Bug Fix](?template=bug_fix.md) - Fixing an existing issue
- ✨ [Feature](?template=feature.md) - Adding new functionality to a module
- 📝 [Documentation](?template=documentation.md) - Improving docs only
---
If you've already started your PR, add `?template=TEMPLATE_NAME.md` to your URL.
### Quick Checklist
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Following contribution guidelines
-22
View File
@@ -1,22 +0,0 @@
## Bug Fix: [Brief Description]
**Module:** `registry/[namespace]/modules/[module-name]`
**Version:** `v1.2.3``v1.2.4`
### Problem
<!-- What was broken? -->
### Solution
<!-- How did you fix it? -->
### Testing
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] Fix verified locally
### Related Issue
<!-- Link to issue if applicable -->
@@ -1,23 +0,0 @@
## Documentation Update
### Description
<!-- What documentation did you improve? -->
### Changes
<!-- What specific changes were made? -->
-
-
### Checklist
- [ ] README validation passes (if applicable)
- [ ] Code examples tested
- [ ] Links verified
- [ ] Formatting consistent
### Context
<!-- Why were these changes needed? -->
-32
View File
@@ -1,32 +0,0 @@
## Feature: [Brief Description]
**Module:** `registry/[namespace]/modules/[module-name]`
**Version:** `v1.2.3``v1.3.0` (or `v2.0.0` for breaking changes)
### Description
<!-- What does this feature add? -->
### Changes
<!-- List key changes -->
-
-
### Breaking Changes
<!-- List any breaking changes (if major version bump) -->
- None / _Remove if not applicable_
### Testing
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] New tests added for feature
- [ ] Backward compatibility maintained
### Documentation
<!-- Did you update the README? -->
@@ -1,25 +0,0 @@
## New Module: [Module Name]
**Module Path:** `registry/[namespace]/modules/[module-name]`
**Initial Version:** `v1.0.0`
### Description
<!-- Brief description of what this module does -->
### Checklist
- [ ] All required files included (`main.tf`, `main.test.ts`, `README.md`)
- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun run fmt`)
- [ ] README has proper frontmatter
- [ ] Icon path is valid
- [ ] Namespace has avatar (if first-time contributor)
### Testing
<!-- How did you test this module? -->
### Additional Notes
<!-- Any special setup or dependencies? -->
-65
View File
@@ -1,65 +0,0 @@
# GitHub Scripts
This directory contains reusable scripts for GitHub Actions workflows.
## version-bump.sh
Extracts version bump logic from GitHub Actions workflows into a reusable script.
### Usage
```bash
./version-bump.sh <bump_type> [base_ref]
```
**Parameters:**
- `bump_type`: Type of version bump (`patch`, `minor`, or `major`)
- `base_ref`: Base reference for diff comparison (default: `origin/main`)
### Examples
```bash
# Bump patch version for modules changed since origin/main
./version-bump.sh patch
# Bump minor version for modules changed since a specific commit
./version-bump.sh minor abc123
# Bump major version for modules changed since a specific branch
./version-bump.sh major origin/develop
```
### What it does
1. **Detects modified modules** from git diff changes
2. **Gets current version** from latest release tag or README
3. **Calculates new version** based on bump type
4. **Updates README versions** in module documentation
5. **Provides summary** of changes and next steps
### Version Detection
- **Tagged modules**: Uses latest `release/namespace/module/vX.Y.Z` tag
- **Untagged modules**: Extracts version from README `version = "X.Y.Z"`
- **New modules**: Start from v1.0.0
### Exit Codes
- `0`: Success (with or without changes)
- `1`: Error (invalid arguments, no modules found, invalid version format, etc.)
### Integration with GitHub Actions
This script is designed to be called from GitHub Actions workflows. See `.github/workflows/version-bump.yaml` for an example implementation that:
- Triggers on PR labels (`version:patch`, `version:minor`, `version:major`)
- Runs the script with appropriate parameters
- Commits any README changes
- Comments on the PR with results
### Notes
- Only updates READMEs that contain version references matching the module source
- Warns about modules missing proper git tags
- Follows semantic versioning (X.Y.Z format)
- Validates all version components are numeric
-24
View File
@@ -1,24 +0,0 @@
name: golangci-lint
on:
push:
branches:
- main
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.1
-213
View File
@@ -1,213 +0,0 @@
version: "2"
linters:
default: none
enable:
- asciicheck
- bidichk
- bodyclose
- dogsled
- dupl
- errcheck
- errname
- errorlint
- exhaustruct
- forcetypeassert
- gocognit
- gocritic
- godot
- gomodguard
- gosec
- govet
- importas
- ineffassign
- makezero
- misspell
- nestif
- nilnil
# - noctx
# - paralleltest
- revive
- staticcheck
# - tparallel
- unconvert
- unused
settings:
dupl:
threshold: 412
godot:
scope: all
capital: true
exhaustruct:
include:
- httpmw\.\w+
- github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params
gocognit:
min-complexity: 300
goconst:
min-len: 4
min-occurrences: 3
gocritic:
enabled-checks:
- badLock
- badRegexp
- boolExprSimplify
- builtinShadow
- builtinShadowDecl
- commentedOutImport
- deferUnlambda
- dupImport
- dynamicFmtString
- emptyDecl
- emptyFallthrough
- emptyStringTest
- evalOrder
- externalErrorReassign
- filepathJoin
- hexLiteral
- httpNoBody
- importShadow
- indexAlloc
- initClause
- methodExprCall
- nestingReduce
- nilValReturn
- preferFilepathJoin
- rangeAppendAll
- regexpPattern
- redundantSprint
- regexpSimplify
- ruleguard
- sliceClear
- sortSlice
- sprintfQuotedString
- sqlQuery
- stringConcatSimplify
- stringXbytes
- todoCommentWithoutDetail
- tooManyResultsChecker
- truncateCmp
- typeAssertChain
- typeDefFirst
- unlabelStmt
- weakCond
- whyNoLint
settings:
ruleguard:
failOn: all
rules: ${base-path}/scripts/rules.go
gosec:
excludes:
- G601
govet:
disable:
- loopclosure
importas:
no-unaliased: true
misspell:
locale: US
ignore-rules:
- trialer
nestif:
min-complexity: 20
revive:
severity: warning
rules:
- name: atomic
- name: bare-return
- name: blank-imports
- name: bool-literal-in-expr
- name: call-to-gc
- name: confusing-results
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
# - name: deep-exit
- name: defer
- name: dot-imports
- name: duplicated-imports
- name: early-return
- name: empty-block
- name: empty-lines
- name: error-naming
- name: error-return
- name: error-strings
- name: errorf
- name: exported
- name: flag-parameter
- name: get-return
- name: identical-branches
- name: if-return
- name: import-shadowing
- name: increment-decrement
- name: indent-error-flow
- name: modifies-value-receiver
- name: package-comments
- name: range
- name: receiver-naming
- name: redefines-builtin-id
- name: string-of-int
- name: struct-tag
- name: superfluous-else
- name: time-naming
- name: unconditional-recursion
- name: unexported-naming
- name: unexported-return
- name: unhandled-error
- name: unnecessary-stmt
- name: unreachable-code
- name: unused-parameter
- name: unused-receiver
- name: var-declaration
- name: var-naming
- name: waitgroup-by-value
staticcheck:
checks:
- all
- SA4006 # Detects redundant assignments
- SA4009 # Detects redundant variable declarations
- SA1019
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
- exhaustruct
- forcetypeassert
path: _test\.go
- linters:
- exhaustruct
path: scripts/*
- linters:
- ALL
path: scripts/rules.go
paths:
- scripts/rules.go
- coderd/database/dbmem
- node_modules
- .git
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0
max-same-issues: 0
fix: true
formatters:
enable:
- goimports
- gofmt
exclusions:
generated: lax
paths:
- scripts/rules.go
- coderd/database/dbmem
- node_modules
- .git
- third_party$
- builtin$
- examples$
+227 -230
View File
@@ -1,297 +1,294 @@
# Contributing to the Coder Registry
# Contributing
Welcome! This guide covers how to contribute to the Coder Registry, whether you're creating a new module or improving an existing one.
## Getting started
## What is the Coder Registry?
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
- [Bun](https://bun.sh/) Used to run tests for each module/template to validate overall functionality and correctness of Terraform output
- [Go](https://go.dev/) Used to validate all README files in the directory. The README content is used to populate [the Registry website](https://registry.coder.com).
## Types of Contributions
### Installing Bun
- **[New Modules](#creating-a-new-module)** - Add support for a new tool or functionality
- **[Existing Modules](#contributing-to-existing-modules)** - Fix bugs, add features, or improve documentation
- **[Bug Reports](#reporting-issues)** - Report problems or request features
To install Bun, you can run this command on Linux/MacOS:
## Setup
### Prerequisites
- Basic Terraform knowledge (for module development)
- Terraform installed ([installation guide](https://developer.hashicorp.com/terraform/install))
- Docker (for running tests)
### Install Dependencies
Install Bun:
```bash
```shell
curl -fsSL https://bun.sh/install | bash
```
Install project dependencies:
Or this command on Windows:
```bash
bun install
```shell
powershell -c "irm bun.sh/install.ps1 | iex"
```
### Understanding Namespaces
Follow the instructions to ensure that Bun is available globally. Once Bun is installed, install all necessary dependencies from the root of the repo:
All modules are organized under `/registry/[namespace]/modules/`. Each contributor gets their own namespace (e.g., `/registry/your-username/modules/`). If a namespace is taken, choose a different unique namespace, but you can still use any display name on the Registry website.
Via NPM:
### Images and Icons
- **Namespace avatars**: Must be named `avatar.png` or `avatar.svg` in `/registry/[namespace]/.images/`
- **Module screenshots/demos**: Use `/registry/[namespace]/.images/` for module-specific images
- **Module icons**: Use the shared `/.icons/` directory at the root for module icons
---
## Creating a New Module
### 1. Create Your Namespace (First Time Only)
If you're a new contributor, create your namespace:
```bash
mkdir -p registry/[your-username]
mkdir -p registry/[your-username]/.images
```shell
npm i
```
#### Add Your Avatar
Via PNPM:
Every namespace must have an avatar. We recommend using your GitHub avatar:
1. Download your GitHub avatar from `https://github.com/[your-username].png`
2. Save it as `avatar.png` in `registry/[your-username]/.images/`
3. This gives you a properly sized, square image that's already familiar to the community
The avatar must be:
- Named exactly `avatar.png` or `avatar.svg`
- Square image (recommended: 400x400px minimum)
- Supported formats: `.png` or `.svg` only
#### Create Your Namespace README
Create `registry/[your-username]/README.md`:
```markdown
---
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar_url: "./.images/avatar.png"
github: "your-username"
linkedin: "https://www.linkedin.com/in/your-username" # Optional
website: "https://yourwebsite.com" # Optional
support_email: "you@example.com" # Optional
status: "community"
---
# Your Name
Brief description of who you are and what you do.
```shell
pnpm i
```
> **Note**: The `avatar_url` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
This repo does not support Yarn.
### 2. Generate Module Files
### Installing Go (optional)
```bash
./scripts/new_module.sh [your-username]/[module-name]
cd registry/[your-username]/modules/[module-name]
This step can be skipped if you are not working on any of the README validation logic. The validation will still run as part of CI.
[Navigate to the official Go Installation page](https://go.dev/doc/install), and install the correct version for your operating system.
Once Go has been installed, verify the installation via:
```shell
go version
```
This script generates:
## Namespaces
- `main.tf` - Terraform configuration template
- `README.md` - Documentation template with frontmatter
- `run.sh` - Script for module execution (can be deleted if not required)
All Coder resources are scoped to namespaces placed at the top level of the `/registry` directory. Any modules or templates must be placed inside a namespace to be accepted as a contribution. For example, all modules created by CoderEmployeeBob would be placed under `/registry/coderemployeebob/modules`, with a subdirectory for each individual module the user has published.
### 3. Build Your Module
If a namespace is already taken, you will need to create a different, unique namespace, but will still be able to choose any display name. (The display name is shown in the Registry website. More info below.)
1. **Edit `main.tf`** to implement your module's functionality
2. **Update `README.md`** with:
- 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
4. **Add any scripts** or additional files your module needs
### Namespace (contributor profile) README files
### 4. Test and Submit
More information about contributor profile README files can be found below.
```bash
# Test your module
bun test -t 'module-name'
### Images
# Format code
bun fmt
Any images needed for either the main namespace directory or a module/template can be placed in a relative `/images` directory at the top of the namespace directory. (e.g., CoderEmployeeBob can have a `/registry/coderemployeebob/images` directory, that can be referenced by the main README file, as well as a README file in `/registry/coderemployeebob/modules/custom_module/README.md`.) This is to minimize the risk of file name conflicts between different users as they add images to help illustrate parts of their README files.
# Commit and create PR
git add .
git commit -m "Add [module-name] module"
git push origin your-branch
## Coder modules
### Adding a new module
> [!WARNING]
> These instructions cannot be followed just yet; the script referenced will be made available shortly. Contributors looking to add modules early will need to create all directories manually.
Once Bun (and possibly Go) have been installed, clone the Coder Registry repository. From there, you can run this script to make it easier to start contributing a new module or template:
```shell
./new.sh USER_NAMESPACE/NAME_OF_NEW_MODULE
```
> **Important**: It is your responsibility to implement tests for every new module. Test your module locally before opening a PR. The testing suite requires Docker containers with the `--network=host` flag, which typically requires running tests on Linux (this flag doesn't work with Docker Desktop on macOS/Windows). macOS users can use [Colima](https://github.com/abiosoft/colima) or [OrbStack](https://orbstack.dev/) instead of Docker Desktop.
You can also create a module file manually by creating the necessary files and directories.
---
### The composition of a Coder module
## Contributing to Existing Modules
Each Coder Module must contain the following files:
### 1. Find the Module
- A `main.tf` file that defines the main Terraform-based functionality
- A `main.test.ts` file that is used to validate that the module works as expected
- A `README.md` file containing required information (listed below)
```bash
find registry -name "*[module-name]*" -type d
You are free to include any additional files in the module, as needed by the module. For example, the [Windows RDP module](https://github.com/coder/registry/tree/main/registry/coder/modules/windows-rdp) contains additional files for injecting specific functionality into a Coder Workspace.
> [!NOTE]
> Some legacy modules do not have test files defined just yet. This will be addressed soon.
### The `main.tf` file
This file defines all core Terraform functionality, to be mixed into your Coder workspaces. More information about [Coder's use of Terraform can be found here](https://coder.com/docs/admin/templates/extending-templates/modules), and [general information about the Terraform language can be found in the official documentation](https://developer.hashicorp.com/terraform/docs).
### The structure of a module README
Validation criteria for module README files is listed below.
### Testing a Module
> [!IMPORTANT]
> It is the responsibility of the module author to implement tests for every new module they wish to contribute. It is expected the author has tested the module locally before opening a PR. Feel free to reference existing test files to get an idea for how to set them up.
All general-purpose test helpers for validating Terraform can be found in the top-level `/testing` directory. The helpers run `terraform apply` on modules that use variables, testing the script output against containers.
When writing a test file, you can import the test utilities via the `~test` import alias:
```ts
// This works regardless of how deeply-nested your test file is in the file
// structure
import {
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";
```
### 2. Make Your Changes
> [!NOTE]
> The testing suite must be able to run docker containers with the `--network=host` flag. This typically requires running the tests on Linux as this flag does not apply to Docker Desktop for MacOS or Windows. MacOS users can work around this by using something like [colima](https://github.com/abiosoft/colima) or [Orbstack](https://orbstack.dev/) instead of Docker Desktop.
**For bug fixes:**
#### Running tests
- Reproduce the issue
- Fix the code in `main.tf`
- Add/update tests
- Update documentation if needed
You can run all tests by running this command from the root of the Registry directory:
**For new features:**
- Add new variables with sensible defaults
- Implement the feature
- Add tests for new functionality
- Update README with new variables
**For documentation:**
- Fix typos and unclear explanations
- Add missing variable documentation
- Improve usage examples
### 3. Test Your Changes
```bash
# Test a specific module
bun test -t 'module-name'
# Test all modules
```shell
bun test
```
### 4. Maintain Backward Compatibility
Note that running _all_ tests can take some time, so you likely don't want to be running this command as part of your core development loop.
- New variables should have default values
- Don't break existing functionality
- Test that minimal configurations still work
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
---
## Submitting Changes
1. **Fork and branch:**
```bash
git checkout -b fix/module-name-issue
```
2. **Commit with clear messages:**
```bash
git commit -m "Fix version parsing in module-name"
```
3. **Open PR with:**
- Clear title describing the change
- What you changed and why
- Any breaking changes
### Using PR Templates
We have different PR templates for different types of contributions. GitHub will show you options to choose from, or you can manually select:
- **New Module**: Use `?template=new_module.md`
- **Bug Fix**: Use `?template=bug_fix.md`
- **Feature**: Use `?template=feature.md`
- **Documentation**: Use `?template=documentation.md`
Example: `https://github.com/coder/registry/compare/main...your-branch?template=new_module.md`
---
## Requirements
### Every Module Must Have
- `main.tf` - Terraform code
- `main.test.ts` - Working tests
- `README.md` - Documentation with frontmatter
### README Frontmatter
Module README frontmatter must include:
```yaml
---
display_name: "Module Name" # Required - Name shown on Registry website
description: "What it does" # Required - Short description
icon: "../../../../.icons/tool.svg" # Required - Path to icon file
verified: false # Optional - Set by maintainers only
tags: ["tag1", "tag2"] # Required - Array of descriptive tags
---
```shell
bun test -t '<regex_pattern>'
```
### README Requirements
To ensure that the module runs predictably in local development, you can update the Terraform source as follows:
All README files must follow these rules:
```tf
module "example" {
# You may need to remove the 'version' field, it is incompatible with some sources.
source = "git::https://github.com/<USERNAME>/<REPO>.git//<MODULE-NAME>?ref=<BRANCH-NAME>"
}
```
- Must have frontmatter section with proper YAML
- Exactly one h1 header directly below frontmatter
- When increasing header levels, increment by one each time
- Use `tf` instead of `hcl` for code blocks
## Updating README files
### Best Practices
This repo uses Go to validate each README file. If you are working with the README files at all (i.e., creating them, modifying them), it is strongly recommended that you install Go (installation instructions mentioned above), so that the files can be validated locally.
- Use descriptive variable names and descriptions
- Include helpful comments
- Test all functionality
- Follow existing code patterns in the module
### Validating all README files
---
To validate all README files throughout the entire repo, you can run the following:
## Versioning Guidelines
```shell
go build ./cmd/readmevalidation && ./readmevalidation
```
After your PR is merged, maintainers will handle the release. Understanding version numbers helps you describe the impact of your changes:
The resulting binary is already part of the `.gitignore` file, but you can remove it with:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
```shell
rm ./readmevalidation
```
**Important**: Always specify the version change in your PR (e.g., `v1.2.3 → v1.2.4`). This helps maintainers create the correct release tag.
### README validation criteria
---
The following criteria exists for two reasons:
## Reporting Issues
1. Content accessibility
2. Having content be designed in a way that's easy for the Registry site build step to use
When reporting bugs, include:
#### General README requirements
- Module name and version
- Expected vs actual behavior
- Minimal reproduction case
- Error messages
- Environment details (OS, Terraform version)
- There must be a frontmatter section.
- There must be exactly one h1 header, and it must be at the very top, directly below the frontmatter.
- The README body (if it exists) must start with an h1 header. No other content (including GitHub-Flavored Markdown alerts) is allowed to be placed above it.
- When increasing the level of a header, the header's level must be incremented by one each time.
- Any `.hcl` code snippets must be labeled as `.tf` snippets instead
---
```txt
\`\`\`tf
Content
\`\`\`
```
## Getting Help
#### Namespace (contributor profile) criteria
- **Examples**: Check `/registry/coder/modules/` for well-structured modules
- **Issues**: Open an issue for technical problems
- **Community**: Reach out to the Coder community for questions
In addition to the general criteria, all README files must have the following:
## Common Pitfalls
- Frontmatter metadata with support for the following fields:
1. **Missing frontmatter** in README
2. **No tests** or broken tests
3. **Hardcoded values** instead of variables
4. **Breaking changes** without defaults
5. **Not running** `bun fmt` before submitting
- `display_name` (required string) The name to use when displaying your user profile in the Coder Registry site.
- `bio` (optional string) A short description of who you are.
- `github` (optional string) Your GitHub handle.
- `avatar_url` (optional string)  A relative/absolute URL pointing to your avatar for the Registry site. It is strongly recommended that you commit avatar images to this repo and reference them via a relative URL.
- `linkedin` (optional string)  A URL pointing to your LinkedIn page.
- `support_email` (optional string) An email for users to reach you at if they need help with a published module/template.
- `status` (string union)  If defined, this must be one of `"community"`, `"partner"`, or `"official"`. `"community"` should be used for the majority of external contributions. `"partner"` is for companies who have a formal business partnership with Coder. `"official"` should be used only by Coder employees.
Happy contributing! 🚀
- The README body (the content that goes directly below the frontmatter) is allowed to be empty, but if it isn't, it must follow all the rules above.
You are free to customize the body of a contributor profile however you like, adding any number of images or information. Its content will never be rendered in the Registry website.
Additional information can be placed in the README file below the content listed above, using any number of headers.
Additional image/video assets can be placed in the same user namespace directory where that user's main content lives.
#### Module criteria
In addition to the general criteria, all README files must have the following:
- Frontmatter that describes metadata for the module:
- `display_name` (required string) This is the name displayed on the Coder Registry website
- `description` (required string) A short description of the module, which is displayed on the Registry website
- `icon` (required string) A relative/absolute URL pointing to the icon to display for the module in the Coder Registry website.
- `verified` (optional boolean) Indicates whether the module has been officially verified by Coder. Please do not set this without approval from a Coder employee.
- `tags` (required string array) A list of metadata tags to describe the module. Used in the Registry site for search and navigation functionality.
- `maintainer_github` (deprecated string)  The name of the creator of the module. This field exists for backwards compatibility with previous versions of the Registry, but going forward, the value will be inferred from the namespace directory.
- `partner_github` (deprecated string) - The name of any additional creators for a module. This field exists for backwards compatibility with previous versions of the Registry, but should not ever be used going forward.
- The following content directly under the h1 header (without another header between them):
- A description of what the module does
- A Terraform snippet for letting other users import the functionality
```tf
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.0.19"
agent_id = coder_agent.example.id
}
```
Additional information can be placed in the README file below the content listed above, using any number of headers.
Additional image/video assets can be placed in one of two places:
1. In the same user namespace directory where that user's main content lives
2. If the image is an icon, it can be placed in the top-level `.icons` directory (this is done because a lot of modules will be based off the same products)
## Releases
The release process involves the following steps:
### 1. Create and merge a new PR
- Create a PR with your module changes
- Get your PR reviewed, approved, and merged into the `main` branch
### 2. Prepare Release (Maintainer Task)
After merging to `main`, a maintainer will:
- Check out the merge commit:
```shell
git checkout MERGE_COMMIT_ID
```
- Create annotated tags for each module that was changed:
```shell
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
```
- Push the tags to origin:
```shell
git push origin release/$namespace/$module/v$version
```
For example, to release version 1.0.14 of the coder/aider module:
```shell
git tag -a "release/coder/aider/v1.0.14" -m "Release coder/aider v1.0.14"
git push origin release/coder/aider/v1.0.14
```
### Version Numbers
Version numbers should follow semantic versioning:
- **Patch version** (1.2.3 → 1.2.4): Bug fixes
- **Minor version** (1.2.3 → 1.3.0): New features, adding inputs, deprecating inputs
- **Major version** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing input types)
### 3. Publishing to Coder Registry
After tags are pushed, the changes will be published to [registry.coder.com](https://registry.coder.com).
> [!NOTE]
> Some data in registry.coder.com is fetched on demand from this repository's `main` branch. This data should update almost immediately after a release, while other changes will take some time to propagate.
-103
View File
@@ -1,103 +0,0 @@
# Maintainer Guide
Quick reference for maintaining the Coder Registry repository.
## Setup
Install Go for README validation:
```bash
# macOS
brew install go
# Linux
sudo apt install golang-go
```
## Daily Tasks
### Review PRs
Check that PRs have:
- [ ] All required files (`main.tf`, `main.test.ts`, `README.md`)
- [ ] Proper frontmatter in README
- [ ] Working tests (`bun test`)
- [ ] Formatted code (`bun run fmt`)
- [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`)
#### Version Guidelines
When reviewing PRs, ensure the version change follows semantic versioning:
- **Patch** (1.2.3 → 1.2.4): Bug fixes
- **Minor** (1.2.3 → 1.3.0): New features, adding inputs
- **Major** (1.2.3 → 2.0.0): Breaking changes (removing inputs, changing types)
PRs should clearly indicate the version change (e.g., `v1.2.3 → v1.2.4`).
### Validate READMEs
```bash
go build ./cmd/readmevalidation && ./readmevalidation
```
## Releases
### Create Release Tags
After merging a PR:
1. Get the new version from the PR (shown as `old → new`)
2. Checkout the merge commit and create the tag:
```bash
# Checkout the merge commit
git checkout MERGE_COMMIT_ID
# Create and push the release tag using the version from the PR
git tag -a "release/$namespace/$module/v$version" -m "Release $namespace/$module v$version"
git push origin release/$namespace/$module/v$version
```
Example: If PR shows `v1.2.3 → v1.2.4`, use `v1.2.4` in the tag.
### Publishing
Changes are automatically published to [registry.coder.com](https://registry.coder.com) after tags are pushed.
## README Requirements
### Module Frontmatter (Required)
```yaml
display_name: "Module Name"
description: "What it does"
icon: "../../../../.icons/tool.svg"
maintainer_github: "username"
partner_github: "partner-name" # Optional - For official partner modules
verified: false # Optional - Set by maintainers only
tags: ["tag1", "tag2"]
```
### Namespace Frontmatter (Required)
```yaml
display_name: "Your Name"
bio: "Brief description of who you are and what you do"
avatar_url: "./.images/avatar.png"
github: "username"
linkedin: "https://www.linkedin.com/in/username" # Optional
website: "https://yourwebsite.com" # Optional
support_email: "you@example.com" # Optional
status: "community" # or "partner", "official"
```
## Common Issues
- **README validation fails**: Check YAML syntax, ensure h1 header after frontmatter
- **Tests fail**: Ensure Docker with `--network=host`, check Terraform syntax
- **Wrong file structure**: Use `./scripts/new_module.sh` for new modules
- **Missing namespace avatar**: Must be `avatar.png` or `avatar.svg` in `.images/` directory
That's it. Keep it simple.
+67 -64
View File
@@ -2,8 +2,9 @@ package main
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
@@ -11,18 +12,10 @@ import (
"slices"
"strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
var (
supportedResourceTypes = []string{"modules", "templates"}
// TODO: This is a holdover from the validation logic used by the Coder Modules repo. It gives us some assurance, but
// realistically, we probably want to parse any Terraform code snippets, and make some deeper guarantees about how it's
// structured. Just validating whether it *can* be parsed as Terraform would be a big improvement.
terraformVersionRe = regexp.MustCompile(`^\s*\bversion\s+=`)
)
var supportedResourceTypes = []string{"modules", "templates"}
type coderResourceFrontmatter struct {
Description string `yaml:"description"`
@@ -34,7 +27,7 @@ type coderResourceFrontmatter struct {
// coderResourceReadme represents a README describing a Terraform resource used
// to help create Coder workspaces. As of 2025-04-15, this encapsulates both
// Coder Modules and Coder Templates.
// Coder Modules and Coder Templates
type coderResourceReadme struct {
resourceType string
filePath string
@@ -44,60 +37,61 @@ type coderResourceReadme struct {
func validateCoderResourceDisplayName(displayName *string) error {
if displayName != nil && *displayName == "" {
return xerrors.New("if defined, display_name must not be empty string")
return errors.New("if defined, display_name must not be empty string")
}
return nil
}
func validateCoderResourceDescription(description string) error {
if description == "" {
return xerrors.New("frontmatter description cannot be empty")
return errors.New("frontmatter description cannot be empty")
}
return nil
}
func isPermittedRelativeURL(checkURL string) bool {
// Would normally be skittish about having relative paths like this, but it should be safe because we have
// guarantees about the structure of the repo, and where this logic will run.
return strings.HasPrefix(checkURL, "./") || strings.HasPrefix(checkURL, "/") || strings.HasPrefix(checkURL, "../../../../.icons")
}
func validateCoderResourceIconURL(iconURL string) []error {
problems := []error{}
if iconURL == "" {
return []error{xerrors.New("icon URL cannot be empty")}
problems = append(problems, errors.New("icon URL cannot be empty"))
return problems
}
errs := []error{}
// If the URL does not have a relative path.
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
if isAbsoluteURL {
if _, err := url.ParseRequestURI(iconURL); err != nil {
errs = append(errs, xerrors.New("absolute icon URL is not correctly formatted"))
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
}
if strings.Contains(iconURL, "?") {
errs = append(errs, xerrors.New("icon URLs cannot contain query parameters"))
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
}
return errs
return problems
}
// If the URL has a relative path.
if !isPermittedRelativeURL(iconURL) {
errs = append(errs, xerrors.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
// Would normally be skittish about having relative paths like this, but it
// should be safe because we have guarantees about the structure of the
// repo, and where this logic will run
isPermittedRelativeURL := strings.HasPrefix(iconURL, "./") ||
strings.HasPrefix(iconURL, "/") ||
strings.HasPrefix(iconURL, "../../../../.icons")
if !isPermittedRelativeURL {
problems = append(problems, fmt.Errorf("relative icon URL %q must either be scoped to that module's directory, or the top-level /.icons directory (this can usually be done by starting the path with \"../../../.icons\")", iconURL))
}
return errs
return problems
}
func validateCoderResourceTags(tags []string) error {
if tags == nil {
return xerrors.New("provided tags array is nil")
return errors.New("provided tags array is nil")
}
if len(tags) == 0 {
return nil
}
// All of these tags are used for the module/template filter controls in the Registry site. Need to make sure they
// can all be placed in the browser URL without issue.
// All of these tags are used for the module/template filter controls in the
// Registry site. Need to make sure they can all be placed in the browser
// URL without issue
invalidTags := []string{}
for _, t := range tags {
if t != url.QueryEscape(t) {
@@ -106,16 +100,21 @@ func validateCoderResourceTags(tags []string) error {
}
if len(invalidTags) != 0 {
return xerrors.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
}
return nil
}
func validateCoderResourceReadmeBody(body string) []error {
var errs []error
// Todo: This is a holdover from the validation logic used by the Coder Modules
// repo. It gives us some assurance, but realistically, we probably want to
// parse any Terraform code snippets, and make some deeper guarantees about how
// it's structured. Just validating whether it *can* be parsed as Terraform
// would be a big improvement.
var terraformVersionRe = regexp.MustCompile("^\\s*\\bversion\\s+=")
func validateCoderResourceReadmeBody(body string) []error {
trimmed := strings.TrimSpace(body)
// TODO: this may cause unexpected behavior since the errors slice may have a 0 length. Add a test.
var errs []error
errs = append(errs, validateReadmeBody(trimmed)...)
foundParagraph := false
@@ -131,8 +130,9 @@ func validateCoderResourceReadmeBody(body string) []error {
lineNum++
nextLine := lineScanner.Text()
// Code assumes that invalid headers would've already been handled by the base validation function, so we don't
// need to check deeper if the first line isn't an h1.
// Code assumes that invalid headers would've already been handled by
// the base validation function, so we don't need to check deeper if the
// first line isn't an h1
if lineNum == 1 {
if !strings.HasPrefix(nextLine, "# ") {
break
@@ -147,7 +147,7 @@ func validateCoderResourceReadmeBody(body string) []error {
terraformCodeBlockCount++
}
if strings.HasPrefix(nextLine, "```hcl") {
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
errs = append(errs, errors.New("all .hcl language references must be converted to .tf"))
}
continue
}
@@ -159,33 +159,35 @@ func validateCoderResourceReadmeBody(body string) []error {
continue
}
// Code assumes that we can treat this case as the end of the "h1 section" and don't need to process any further lines.
// Code assumes that we can treat this case as the end of the "h1
// section" and don't need to process any further lines
if lineNum > 1 && strings.HasPrefix(nextLine, "#") {
break
}
// Code assumes that if we've reached this point, the only other options are:
// (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset references made via [] syntax.
// Code assumes that if we've reached this point, the only other options
// are: (1) empty spaces, (2) paragraphs, (3) HTML, and (4) asset
// references made via [] syntax
trimmedLine := strings.TrimSpace(nextLine)
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
foundParagraph = foundParagraph || isParagraph
}
if terraformCodeBlockCount == 0 {
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
errs = append(errs, errors.New("did not find Terraform code block within h1 section"))
} else {
if terraformCodeBlockCount > 1 {
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
errs = append(errs, errors.New("cannot have more than one Terraform code block in h1 section"))
}
if !foundTerraformVersionRef {
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
errs = append(errs, errors.New("did not find Terraform code block that specifies 'version' field"))
}
}
if !foundParagraph {
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
errs = append(errs, errors.New("did not find paragraph within h1 section"))
}
if isInsideCodeBlock {
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
errs = append(errs, errors.New("code blocks inside h1 section do not all terminate before end of file"))
}
return errs
@@ -218,12 +220,12 @@ func validateCoderResourceReadme(rm coderResourceReadme) []error {
func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceReadme, error) {
fm, body, err := separateFrontmatter(rm.rawText)
if err != nil {
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
}
yml := coderResourceFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
}
return coderResourceReadme{
@@ -248,21 +250,21 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
}
if len(yamlParsingErrs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlParsingErrs,
}
}
yamlValidationErrors := []error{}
for _, readme := range resources {
errs := validateCoderResourceReadme(readme)
if len(errs) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errs...)
errors := validateCoderResourceReadme(readme)
if len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
}
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlValidationErrors,
}
}
@@ -271,8 +273,8 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
}
// Todo: Need to beef up this function by grabbing each image/video URL from
// the body's AST.
func validateCoderResourceRelativeURLs(_ map[string]coderResourceReadme) error {
// the body's AST
func validateCoderResourceRelativeUrls(resources map[string]coderResourceReadme) error {
return nil
}
@@ -319,7 +321,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
if len(errs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseFile,
phase: validationPhaseFileLoad,
errors: errs,
}
}
@@ -328,7 +330,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
func validateAllCoderResourceFilesOfType(resourceType string) error {
if !slices.Contains(supportedResourceTypes, resourceType) {
return xerrors.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
return fmt.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
}
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
@@ -336,16 +338,17 @@ func validateAllCoderResourceFilesOfType(resourceType string) error {
return err
}
logger.Info(context.Background(), "rocessing README files", "num_files", len(allReadmeFiles))
log.Printf("Processing %d README files\n", len(allReadmeFiles))
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
if err != nil {
return err
}
logger.Info(context.Background(), "rocessed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType)
if err := validateCoderResourceRelativeURLs(resources); err != nil {
err = validateCoderResourceRelativeUrls(resources)
if err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "type", resourceType)
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType)
return nil
}
+69 -58
View File
@@ -1,27 +1,30 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"net/url"
"os"
"path"
"slices"
"strings"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
var validContributorStatuses = []string{"official", "partner", "community"}
type contributorProfileFrontmatter struct {
DisplayName string `yaml:"display_name"`
Bio string `yaml:"bio"`
ContributorStatus string `yaml:"status"`
AvatarURL *string `yaml:"avatar"`
LinkedinURL *string `yaml:"linkedin"`
WebsiteURL *string `yaml:"website"`
SupportEmail *string `yaml:"support_email"`
DisplayName string `yaml:"display_name"`
Bio string `yaml:"bio"`
ContributorStatus string `yaml:"status"`
// Script assumes that if avatar URL is nil, the Registry site build step
// will backfill the value with the user's GitHub avatar URL
AvatarURL *string `yaml:"avatar"`
LinkedinURL *string `yaml:"linkedin"`
WebsiteURL *string `yaml:"website"`
SupportEmail *string `yaml:"support_email"`
}
type contributorProfileReadme struct {
@@ -32,7 +35,7 @@ type contributorProfileReadme struct {
func validateContributorDisplayName(displayName string) error {
if displayName == "" {
return xerrors.New("missing display_name")
return fmt.Errorf("missing display_name")
}
return nil
@@ -44,15 +47,12 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
}
if _, err := url.ParseRequestURI(*linkedinURL); err != nil {
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
return fmt.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
}
return nil
}
// validateContributorSupportEmail does best effort validation of a contributors email address. We can't 100% validate
// that this is correct without actually sending an email, especially because some contributors are individual developers
// and we don't want to do that on every single run of the CI pipeline. The best we can do is verify the general structure.
func validateContributorSupportEmail(email *string) []error {
if email == nil {
return nil
@@ -60,30 +60,34 @@ func validateContributorSupportEmail(email *string) []error {
errs := []error{}
// Can't 100% validate that this is correct without actually sending
// an email, and especially with some contributors being individual
// developers, we don't want to do that on every single run of the CI
// pipeline. Best we can do is verify the general structure
username, server, ok := strings.Cut(*email, "@")
if !ok {
errs = append(errs, xerrors.Errorf("email address %q is missing @ symbol", *email))
errs = append(errs, fmt.Errorf("email address %q is missing @ symbol", *email))
return errs
}
if username == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing username", *email))
errs = append(errs, fmt.Errorf("email address %q is missing username", *email))
}
domain, tld, ok := strings.Cut(server, ".")
if !ok {
errs = append(errs, xerrors.Errorf("email address %q is missing period for server segment", *email))
errs = append(errs, fmt.Errorf("email address %q is missing period for server segment", *email))
return errs
}
if domain == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing domain", *email))
errs = append(errs, fmt.Errorf("email address %q is missing domain", *email))
}
if tld == "" {
errs = append(errs, xerrors.Errorf("email address %q is missing top-level domain", *email))
errs = append(errs, fmt.Errorf("email address %q is missing top-level domain", *email))
}
if strings.Contains(*email, "?") {
errs = append(errs, xerrors.New("email is not allowed to contain query parameters"))
errs = append(errs, errors.New("email is not allowed to contain query parameters"))
}
return errs
@@ -95,7 +99,7 @@ func validateContributorWebsite(websiteURL *string) error {
}
if _, err := url.ParseRequestURI(*websiteURL); err != nil {
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
return fmt.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
}
return nil
@@ -103,32 +107,35 @@ func validateContributorWebsite(websiteURL *string) error {
func validateContributorStatus(status string) error {
if !slices.Contains(validContributorStatuses, status) {
return xerrors.Errorf("contributor status %q is not valid", status)
return fmt.Errorf("contributor status %q is not valid", status)
}
return nil
}
// Can't validate the image actually leads to a valid resource in a pure function, but can at least catch obvious problems.
// Can't validate the image actually leads to a valid resource in a pure
// function, but can at least catch obvious problems
func validateContributorAvatarURL(avatarURL *string) []error {
if avatarURL == nil {
return nil
}
errs := []error{}
if *avatarURL == "" {
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string"))
return errs
}
errs := []error{}
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL.
// Have to use .Parse instead of .ParseRequestURI because this is the
// one field that's allowed to be a relative URL
if _, err := url.Parse(*avatarURL); err != nil {
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
errs = append(errs, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
}
if strings.Contains(*avatarURL, "?") {
errs = append(errs, xerrors.New("avatar URL is not allowed to contain search parameters"))
errs = append(errs, errors.New("avatar URL is not allowed to contain search parameters"))
}
var matched bool
matched := false
for _, ff := range supportedAvatarFileFormats {
matched = strings.HasSuffix(*avatarURL, ff)
if matched {
@@ -138,7 +145,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
if !matched {
segments := strings.Split(*avatarURL, ".")
fileExtension := segments[len(segments)-1]
errs = append(errs, xerrors.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
errs = append(errs, fmt.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
}
return errs
@@ -173,12 +180,12 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
fm, _, err := separateFrontmatter(rm.rawText)
if err != nil {
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
}
yml := contributorProfileFrontmatter{}
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
}
return contributorProfileReadme{
@@ -199,28 +206,29 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
}
if prev, alreadyExists := profilesByNamespace[p.namespace]; alreadyExists {
yamlParsingErrors = append(yamlParsingErrors, xerrors.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
yamlParsingErrors = append(yamlParsingErrors, fmt.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
continue
}
profilesByNamespace[p.namespace] = p
}
if len(yamlParsingErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlParsingErrors,
}
}
yamlValidationErrors := []error{}
for _, p := range profilesByNamespace {
if errors := validateContributorReadme(p); len(errors) > 0 {
errors := validateContributorReadme(p)
if len(errors) > 0 {
yamlValidationErrors = append(yamlValidationErrors, errors...)
continue
}
}
if len(yamlValidationErrors) != 0 {
return nil, validationPhaseError{
phase: validationPhaseReadme,
phase: validationPhaseReadmeParsing,
errors: yamlValidationErrors,
}
}
@@ -236,13 +244,12 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
allReadmeFiles := []readme{}
errs := []error{}
dirPath := ""
for _, e := range dirEntries {
dirPath := path.Join(rootRegistryPath, e.Name())
if !e.IsDir() {
continue
}
dirPath = path.Join(rootRegistryPath, e.Name())
readmePath := path.Join(dirPath, "README.md")
rmBytes, err := os.ReadFile(readmePath)
if err != nil {
@@ -257,7 +264,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
if len(errs) != 0 {
return nil, validationPhaseError{
phase: validationPhaseFile,
phase: validationPhaseFileLoad,
errors: errs,
}
}
@@ -265,31 +272,34 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
return allReadmeFiles, nil
}
func validateContributorRelativeURLs(contributors map[string]contributorProfileReadme) error {
// This function only validates relative avatar URLs for now, but it can be beefed up to validate more in the future.
var errs []error
func validateContributorRelativeUrls(contributors map[string]contributorProfileReadme) error {
// This function only validates relative avatar URLs for now, but it can be
// beefed up to validate more in the future
errs := []error{}
for _, con := range contributors {
// If the avatar URL is missing, we'll just assume that the Registry site build step will take care of filling
// in the data properly.
// If the avatar URL is missing, we'll just assume that the Registry
// site build step will take care of filling in the data properly
if con.frontmatter.AvatarURL == nil {
continue
}
if !strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || !strings.HasPrefix(*con.frontmatter.AvatarURL, "/") {
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") ||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/")
if !isRelativeURL {
continue
}
isAvatarInApprovedSpot := strings.HasPrefix(*con.frontmatter.AvatarURL, "./.images/") ||
strings.HasPrefix(*con.frontmatter.AvatarURL, ".images/")
if !isAvatarInApprovedSpot {
errs = append(errs, xerrors.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
if strings.HasPrefix(*con.frontmatter.AvatarURL, "..") {
errs = append(errs, fmt.Errorf("%q: relative avatar URLs cannot be placed outside a user's namespaced directory", con.filePath))
continue
}
absolutePath := strings.TrimSuffix(con.filePath, "README.md") + *con.frontmatter.AvatarURL
if _, err := os.ReadFile(absolutePath); err != nil {
errs = append(errs, xerrors.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, absolutePath))
absolutePath := strings.TrimSuffix(con.filePath, "README.md") +
*con.frontmatter.AvatarURL
_, err := os.ReadFile(absolutePath)
if err != nil {
errs = append(errs, fmt.Errorf("%q: relative avatar path %q does not point to image in file system", con.filePath, *con.frontmatter.AvatarURL))
}
}
@@ -297,7 +307,7 @@ func validateContributorRelativeURLs(contributors map[string]contributorProfileR
return nil
}
return validationPhaseError{
phase: validationPhaseCrossReference,
phase: validationPhaseAssetCrossReference,
errors: errs,
}
}
@@ -308,18 +318,19 @@ func validateAllContributorFiles() error {
return err
}
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
log.Printf("Processing %d README files\n", len(allReadmeFiles))
contributors, err := parseContributorFiles(allReadmeFiles)
if err != nil {
return err
}
logger.Info(context.Background(), "processed README files as valid contributor profiles", "num_contributors", len(contributors))
log.Printf("Processed %d README files as valid contributor profiles", len(contributors))
if err := validateContributorRelativeURLs(contributors); err != nil {
err = validateContributorRelativeUrls(contributors)
if err != nil {
return err
}
logger.Info(context.Background(), "all relative URLs for READMEs are valid")
log.Println("All relative URLs for READMEs are valid")
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
return nil
}
+6 -8
View File
@@ -1,13 +1,11 @@
package main
import (
"fmt"
import "fmt"
"golang.org/x/xerrors"
)
// validationPhaseError represents an error that occurred during a specific phase of README validation. It should be
// used to collect ALL validation errors that happened during a specific phase, rather than the first one encountered.
// validationPhaseError represents an error that occurred during a specific
// phase of README validation. It should be used to collect ALL validation
// errors that happened during a specific phase, rather than the first one
// encountered.
type validationPhaseError struct {
phase validationPhase
errors []error
@@ -26,5 +24,5 @@ func (vpe validationPhaseError) Error() string {
}
func addFilePathToError(filePath string, err error) error {
return xerrors.Errorf("%q: %v", filePath, err)
return fmt.Errorf("%q: %v", filePath, err)
}
+12 -15
View File
@@ -6,28 +6,25 @@
package main
import (
"context"
"fmt"
"log"
"os"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
)
var logger = slog.Make(sloghuman.Sink(os.Stdout))
func main() {
logger.Info(context.Background(), "starting README validation")
log.Println("Starting README validation")
// If there are fundamental problems with how the repo is structured, we can't make any guarantees that any further
// validations will be relevant or accurate.
err := validateRepoStructure()
if err != nil {
logger.Error(context.Background(), "error when validating the repo structure", "error", err.Error())
// If there are fundamental problems with how the repo is structured, we
// can't make any guarantees that any further validations will be relevant
// or accurate
repoErr := validateRepoStructure()
if repoErr != nil {
log.Println(repoErr)
os.Exit(1)
}
var errs []error
err = validateAllContributorFiles()
err := validateAllContributorFiles()
if err != nil {
errs = append(errs, err)
}
@@ -37,11 +34,11 @@ func main() {
}
if len(errs) == 0 {
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
os.Exit(0)
}
for _, err := range errs {
logger.Error(context.Background(), err.Error())
fmt.Println(err)
}
os.Exit(1)
}
+78 -70
View File
@@ -2,65 +2,35 @@ package main
import (
"bufio"
"errors"
"fmt"
"regexp"
"strings"
"golang.org/x/xerrors"
)
// validationPhase represents a specific phase during README validation. It is expected that each phase is discrete, and
// errors during one will prevent a future phase from starting.
type validationPhase string
const rootRegistryPath = "./registry"
const (
rootRegistryPath = "./registry"
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// --- validationPhases ---
// validationPhaseStructure indicates when the entire Registry
// directory is being verified for having all files be placed in the file
// system as expected.
validationPhaseStructure validationPhase = "File structure validation"
// ValidationPhaseFile indicates when README files are being read from
// the file system.
validationPhaseFile validationPhase = "Filesystem reading"
// ValidationPhaseReadme indicates when a README's frontmatter is
// being parsed as YAML. This phase does not include YAML validation.
validationPhaseReadme validationPhase = "README parsing"
// ValidationPhaseCrossReference indicates when a README's frontmatter
// is having all its relative URLs be validated for whether they point to
// valid resources.
validationPhaseCrossReference validationPhase = "Cross-referencing relative asset URLs"
// --- end of validationPhases ---.
)
var (
supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
// Matches markdown headers, must be at the beginning of a line, such as "# " or "### ".
readmeHeaderRe = regexp.MustCompile(`^(#+)(\s*)`)
)
// readme represents a single README file within the repo (usually within the top-level "/registry" directory).
// readme represents a single README file within the repo (usually within the
// top-level "/registry" directory).
type readme struct {
filePath string
rawText string
}
// separateFrontmatter attempts to separate a README file's frontmatter content from the main README body, returning
// both values in that order. It does not validate whether the structure of the frontmatter is valid (i.e., that it's
// separateFrontmatter attempts to separate a README file's frontmatter content
// from the main README body, returning both values in that order. It does not
// validate whether the structure of the frontmatter is valid (i.e., that it's
// structured as YAML).
func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBody string, err error) {
func separateFrontmatter(readmeText string) (string, string, error) {
if readmeText == "" {
return "", "", xerrors.New("README is empty")
return "", "", errors.New("README is empty")
}
const fence = "---"
var fm strings.Builder
var body strings.Builder
fm := ""
body := ""
fenceCount := 0
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
@@ -70,43 +40,48 @@ func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBod
fenceCount++
continue
}
// Break early if the very first line wasn't a fence, because then we know for certain that the README has problems.
// Break early if the very first line wasn't a fence, because then we
// know for certain that the README has problems
if fenceCount == 0 {
break
}
// It should be safe to trim each line of the frontmatter on a per-line basis, because there shouldn't be any
// extra meaning attached to the indentation. The same does NOT apply to the README; best we can do is gather
// all the lines and then trim around it.
// It should be safe to trim each line of the frontmatter on a per-line
// basis, because there shouldn't be any extra meaning attached to the
// indentation. The same does NOT apply to the README; best we can do is
// gather all the lines, and then trim around it
if inReadmeBody := fenceCount >= 2; inReadmeBody {
fmt.Fprintf(&body, "%s\n", nextLine)
body += nextLine + "\n"
} else {
fmt.Fprintf(&fm, "%s\n", strings.TrimSpace(nextLine))
fm += strings.TrimSpace(nextLine) + "\n"
}
}
if fenceCount < 2 {
return "", "", xerrors.New("README does not have two sets of frontmatter fences")
return "", "", errors.New("README does not have two sets of frontmatter fences")
}
if fm.Len() == 0 {
return "", "", xerrors.New("readme has frontmatter fences but no frontmatter content")
if fm == "" {
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
}
return fm.String(), strings.TrimSpace(body.String()), nil
return fm, strings.TrimSpace(body), nil
}
// TODO: This seems to work okay for now, but the really proper way of doing this is by parsing this as an AST, and then
// checking the resulting nodes.
var readmeHeaderRe = regexp.MustCompile("^(#{1,})(\\s*)")
// Todo: This seems to work okay for now, but the really proper way of doing
// this is by parsing this as an AST, and then checking the resulting nodes
func validateReadmeBody(body string) []error {
trimmed := strings.TrimSpace(body)
if trimmed == "" {
return []error{xerrors.New("README body is empty")}
return []error{errors.New("README body is empty")}
}
// If the very first line of the README doesn't start with an ATX-style H1 header, there's a risk that the rest of the
// validation logic will break, since we don't have many guarantees about how the README is actually structured.
// If the very first line of the README, there's a risk that the rest of the
// validation logic will break, since we don't have many guarantees about
// how the README is actually structured
if !strings.HasPrefix(trimmed, "# ") {
return []error{xerrors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
return []error{errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
}
var errs []error
@@ -118,8 +93,9 @@ func validateReadmeBody(body string) []error {
for lineScanner.Scan() {
nextLine := lineScanner.Text()
// Have to check this because a lot of programming languages support # comments (including Terraform), and
// without any context, there's no way to tell the difference between a markdown header and code comment.
// Have to check this because a lot of programming languages support #
// comments (including Terraform), and without any context, there's no
// way to tell the difference between a markdown header and code comment
if strings.HasPrefix(nextLine, "```") {
isInCodeBlock = !isInCodeBlock
continue
@@ -133,9 +109,9 @@ func validateReadmeBody(body string) []error {
continue
}
// In the Markdown spec it is mandatory to have a space following the header # symbol(s).
if headerGroups[2] == "" {
errs = append(errs, xerrors.New("header does not have space between header characters and main header text"))
spaceAfterHeader := headerGroups[2]
if spaceAfterHeader == "" {
errs = append(errs, errors.New("header does not have space between header characters and main header text"))
}
nextHeaderLevel := len(headerGroups[1])
@@ -145,26 +121,58 @@ func validateReadmeBody(body string) []error {
continue
}
// If we have obviously invalid headers, it's not really safe to keep proceeding with the rest of the content.
// If we have obviously invalid headers, it's not really safe to keep
// proceeding with the rest of the content
if nextHeaderLevel == 1 {
errs = append(errs, xerrors.New("READMEs cannot contain more than h1 header"))
errs = append(errs, errors.New("READMEs cannot contain more than h1 header"))
break
}
if nextHeaderLevel > 6 {
errs = append(errs, xerrors.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
errs = append(errs, fmt.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
break
}
// This is something we need to enforce for accessibility, not just for the Registry website, but also when
// users are viewing the README files in the GitHub web view.
// This is something we need to enforce for accessibility, not just for
// the Registry website, but also when users are viewing the README
// files in the GitHub web view
if nextHeaderLevel > latestHeaderLevel && nextHeaderLevel != (latestHeaderLevel+1) {
errs = append(errs, xerrors.New("headers are not allowed to increase more than 1 level at a time"))
errs = append(errs, fmt.Errorf("headers are not allowed to increase more than 1 level at a time"))
continue
}
// As long as the above condition passes, there's no problems with going up a header level or going down 1+ header levels.
// As long as the above condition passes, there's no problems with
// going up a header level or going down 1+ header levels
latestHeaderLevel = nextHeaderLevel
}
return errs
}
// validationPhase represents a specific phase during README validation. It is
// expected that each phase is discrete, and errors during one will prevent a
// future phase from starting.
type validationPhase string
const (
// validationPhaseFileStructureValidation indicates when the entire Registry
// directory is being verified for having all files be placed in the file
// system as expected.
validationPhaseFileStructureValidation validationPhase = "File structure validation"
// validationPhaseFileLoad indicates when README files are being read from
// the file system
validationPhaseFileLoad = "Filesystem reading"
// validationPhaseReadmeParsing indicates when a README's frontmatter is
// being parsed as YAML. This phase does not include YAML validation.
validationPhaseReadmeParsing = "README parsing"
// validationPhaseReadmeValidation indicates when a README's frontmatter is
// being validated as proper YAML with expected keys.
validationPhaseReadmeValidation = "README validation"
// validationPhaseAssetCrossReference indicates when a README's frontmatter
// is having all its relative URLs be validated for whether they point to
// valid resources.
validationPhaseAssetCrossReference = "Cross-referencing relative asset URLs"
)
+41 -28
View File
@@ -2,60 +2,69 @@ package main
import (
"errors"
"fmt"
"os"
"path"
"slices"
"strings"
"golang.org/x/xerrors"
)
var supportedUserNameSpaceDirectories = append(supportedResourceTypes, ".icons", ".images")
var supportedUserNameSpaceDirectories = append(supportedResourceTypes[:], ".icons", ".images")
func validateCoderResourceSubdirectory(dirPath string) []error {
errs := []error{}
subDir, err := os.Stat(dirPath)
if err != nil {
// It's valid for a specific resource directory not to exist. It's just that if it does exist, it must follow specific rules.
// It's valid for a specific resource directory not to exist. It's just
// that if it does exist, it must follow specific rules
if !errors.Is(err, os.ErrNotExist) {
return []error{addFilePathToError(dirPath, err)}
errs = append(errs, addFilePathToError(dirPath, err))
}
return errs
}
if !subDir.IsDir() {
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
errs = append(errs, fmt.Errorf("%q: path is not a directory", dirPath))
return errs
}
files, err := os.ReadDir(dirPath)
if err != nil {
return []error{addFilePathToError(dirPath, err)}
errs = append(errs, addFilePathToError(dirPath, err))
return errs
}
errs := []error{}
for _, f := range files {
// The .coder subdirectories are sometimes generated as part of Bun tests. These subdirectories will never be
// committed to the repo, but in the off chance that they don't get cleaned up properly, we want to skip over them.
// The .coder subdirectories are sometimes generated as part of Bun
// tests. These subdirectories will never be committed to the repo, but
// in the off chance that they don't get cleaned up properly, we want to
// skip over them
if !f.IsDir() || f.Name() == ".coder" {
continue
}
resourceReadmePath := path.Join(dirPath, f.Name(), "README.md")
if _, err := os.Stat(resourceReadmePath); err != nil {
_, err := os.Stat(resourceReadmePath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
errs = append(errs, fmt.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
} else {
errs = append(errs, addFilePathToError(resourceReadmePath, err))
}
}
mainTerraformPath := path.Join(dirPath, f.Name(), "main.tf")
if _, err := os.Stat(mainTerraformPath); err != nil {
_, err = os.Stat(mainTerraformPath)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
errs = append(errs, fmt.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
} else {
errs = append(errs, addFilePathToError(mainTerraformPath, err))
}
}
}
return errs
}
@@ -69,12 +78,13 @@ func validateRegistryDirectory() []error {
for _, d := range userDirs {
dirPath := path.Join(rootRegistryPath, d.Name())
if !d.IsDir() {
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
allErrs = append(allErrs, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
continue
}
contributorReadmePath := path.Join(dirPath, "README.md")
if _, err := os.Stat(contributorReadmePath); err != nil {
_, err := os.Stat(contributorReadmePath)
if err != nil {
allErrs = append(allErrs, err)
}
@@ -85,7 +95,8 @@ func validateRegistryDirectory() []error {
}
for _, f := range files {
// TODO: Decide if there's anything more formal that we want to ensure about non-directories scoped to user namespaces.
// Todo: Decide if there's anything more formal that we want to
// ensure about non-directories scoped to user namespaces
if !f.IsDir() {
continue
}
@@ -94,12 +105,13 @@ func validateRegistryDirectory() []error {
filePath := path.Join(dirPath, segment)
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
allErrs = append(allErrs, fmt.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
continue
}
if slices.Contains(supportedResourceTypes, segment) {
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
errs := validateCoderResourceSubdirectory(filePath)
if len(errs) != 0 {
allErrs = append(allErrs, errs...)
}
}
@@ -110,19 +122,20 @@ func validateRegistryDirectory() []error {
}
func validateRepoStructure() error {
var errs []error
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
errs = append(errs, vrdErrs...)
var problems []error
if errs := validateRegistryDirectory(); len(errs) != 0 {
problems = append(problems, errs...)
}
if _, err := os.Stat("./.icons"); err != nil {
errs = append(errs, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
_, err := os.Stat("./.icons")
if err != nil {
problems = append(problems, errors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
}
if len(errs) != 0 {
if len(problems) != 0 {
return validationPhaseError{
phase: validationPhaseStructure,
errors: errs,
phase: validationPhaseFileStructureValidation,
errors: problems,
}
}
return nil
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"encoding/json"
"fmt"
)
type terraformLifecycleCondition struct {
// Any terraform expression that evaluates to a boolean
expression string
errorMessage *string
}
type terraformLifecycle struct {
createBeforeDestroy bool
preventDestroy bool
ignoreChanges []string
replaceTriggeredBy []string
preCondition *terraformLifecycleCondition
postCondition *terraformLifecycleCondition
}
type terraformForEachKind string
const (
terraformForEachKindMap terraformForEachKind = "map"
terraformForEachKindSet terraformForEachKind = "set"
)
type terraformForEach struct {
kind terraformForEachKind
// If the kind is "map", all values should be guaranteed to be a definite
// string for each key. If it's "set", all values should be nil
values map[string]*string
}
type terraformVariableKind string
const (
terraformVariableKindString terraformVariableKind = "string"
terraformVariableKindNumber terraformVariableKind = "number"
terraformVariableKindBool terraformVariableKind = "bool"
terraformVariableKindList terraformVariableKind = "list"
terraformVariableKindMap terraformVariableKind = "map"
terraformVariableKindSet terraformVariableKind = "set"
// This value kind is incredibly rare. It corresponds to any variable with
// a null value but no defined type
terraformVariableKindUnknown terraformVariableKind = "unknown"
)
type terraformVariable struct {
kind terraformVariableKind
value any
}
// coderTerraformModule represents the values that can be parsed from a
// Terraform module that are relevant to a Coder deployment. Most of the fields
// are derived from the main Terraform spec, but some additional Coder-specific
// fields are appended, too, for convenience
type coderTerraformModule struct {
// Corresponds to the optional `lifecycle` metadata field available to all
// resource types
Lifecycle *terraformLifecycle `json:"lifecycle"`
// Corresponds to the optional `for_each` metadata field available to all
// resource types
ForEach *terraformForEach `json:"for_each"`
// Corresponds to the `source` field in a module block
ModuleSource string `json:"module_source"`
// Corresponds to the `version` field in Terraform module blocks. Note that
// while the Terraform spec marks this field as optional, Coder requires
// that one always be defined.
Version string `json:"version"`
// Corresponds to the optional `provider` field in a module block
Provider *string `json:"provider"`
// Corresponds to optional `depends_on` field for module blocks
DependsOn []string `json:"depends_on"`
// Corresponds to the `count` field for any Terraform resource type. It
// defines the number of resource instances to create when using Terraform
// Apply.
InstanceCount int `json:"instance_count"`
// Corresponds to `agent_id`` field in a module block. Terraform doesn't
// have any built-in concept of an agent_id, but it's needed to make a
// module work with a Coder deployment
AgentID string `json:"agent_id"`
// Captures all other arbitrary values defined for a Terraform module block.
// Note that while Terraform itself has you define all other fields at the
// same level as the well-known/official fields, they've been isolated into
// a map for the Go struct definition to improve type-safety
Values map[string]terraformVariable `json:"values"`
// The raw Terraform snippet used to derive the coderTerraformModule struct
SourceCode string `json:"source_code"`
}
var _ json.Marshaler = &coderTerraformModule{}
func (ctmCopy coderTerraformModule) MarshalJSON() ([]byte, error) {
if ctmCopy.Lifecycle != nil {
lCopy := &terraformLifecycle{
createBeforeDestroy: ctmCopy.Lifecycle.createBeforeDestroy,
preventDestroy: ctmCopy.Lifecycle.preventDestroy,
ignoreChanges: ctmCopy.Lifecycle.ignoreChanges,
replaceTriggeredBy: ctmCopy.Lifecycle.replaceTriggeredBy,
preCondition: ctmCopy.Lifecycle.preCondition,
postCondition: ctmCopy.Lifecycle.postCondition,
}
// Make sure that both slices always get serialized as JSON null if
// they're empty. Serializing as an empty JSON array has no extra
// semantics
if len(lCopy.ignoreChanges) == 0 {
lCopy.ignoreChanges = nil
}
if len(lCopy.replaceTriggeredBy) == 0 {
lCopy.replaceTriggeredBy = nil
}
ctmCopy.Lifecycle = lCopy
}
if ctmCopy.ForEach != nil {
feCopy := &terraformForEach{
kind: ctmCopy.ForEach.kind,
values: ctmCopy.ForEach.values,
}
// Make sure that the values map is NEVER serialized as JSON null
if feCopy.values == nil {
feCopy.values = make(map[string]*string)
}
ctmCopy.ForEach = feCopy
}
if len(ctmCopy.DependsOn) == 0 {
ctmCopy.DependsOn = nil
}
if ctmCopy.Values == nil {
ctmCopy.Values = make(map[string]terraformVariable)
}
for k, tfValue := range ctmCopy.Values {
switch tfValue.kind {
case terraformVariableKindString, terraformVariableKindBool, terraformVariableKindNumber, terraformVariableKindUnknown:
continue
case terraformVariableKindSet, terraformVariableKindList:
recast, ok := tfValue.value.([]any)
if !ok {
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as set/list", k, tfValue.kind)
}
if recast == nil {
ctmCopy.Values[k] = terraformVariable{
kind: tfValue.kind,
value: []any{},
}
}
case terraformVariableKindMap:
recast, ok := tfValue.value.(map[string]any)
if !ok {
return nil, fmt.Errorf("unable to process terraform variable %q of kind %q as map", k, tfValue.kind)
}
if recast == nil {
ctmCopy.Values[k] = terraformVariable{
kind: tfValue.kind,
value: make(map[string]any),
}
}
}
}
return json.Marshal(ctmCopy)
}
+4 -4
View File
@@ -15,7 +15,7 @@ tags: [helper]
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
version = "1.0.2"
}
```
@@ -31,7 +31,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -49,7 +49,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "MODULE_NAME" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
version = "1.0.2"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -65,7 +65,7 @@ Run code-server in the background, don't fetch it from GitHub:
```tf
module "MODULE_NAME" {
source = "registry.coder.com/NAMESPACE/MODULE_NAME/coder"
version = "1.0.0"
version = "1.0.2"
agent_id = coder_agent.example.id
offline = true
}
+2 -1
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -105,3 +105,4 @@ data "coder_parameter" "MODULE_NAME" {
}
}
}
+1 -22
View File
@@ -2,25 +2,4 @@ module coder.com/coder-registry
go 1.23.2
require (
cdr.dev/slog v1.6.1
github.com/quasilyte/go-ruleguard/dsl v0.3.22
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.7.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
)
require gopkg.in/yaml.v3 v3.0.1
-76
View File
@@ -1,79 +1,3 @@
cdr.dev/slog v1.6.1 h1:IQjWZD0x6//sfv5n+qEhbu3wBkmtBQY5DILXNvMaIv4=
cdr.dev/slog v1.6.1/go.mod h1:eHEYQLaZvxnIAXC+XdTSNLb/kgA/X2RVSF72v5wsxEI=
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/logging v1.7.0 h1:CJYxlNNNNAMkHp9em/YEXcfJg+rPDg7YfwoRpMU+t5I=
cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M=
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e h1:xIXmWJ303kJCuogpj0bHq+dcjcZHU+XFyc1I0Yl9cRg=
google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130 h1:XVeBY8d/FaK4848myy41HBqnDwvxeV3zMZhwN1TvAMU=
google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 KiB

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 155 KiB

-1
View File
@@ -2,7 +2,6 @@
display_name: Coder
bio: Coder provisions cloud development environments via Terraform, supporting Linux, macOS, Windows, X86, ARM, Kubernetes and more.
github: coder
avatar: ./.images/avatar.png
linkedin: https://www.linkedin.com/company/coderhq
website: https://www.coder.com
status: official
+6 -6
View File
@@ -14,7 +14,7 @@ Run [Aider](https://aider.chat) AI pair programming in your workspace. This modu
```tf
module "aider" {
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
}
```
@@ -69,7 +69,7 @@ variable "anthropic_api_key" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
}
@@ -94,7 +94,7 @@ variable "openai_api_key" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
use_tmux = true
ai_provider = "openai"
@@ -115,7 +115,7 @@ variable "custom_api_key" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_provider = "custom"
custom_env_var_name = "MY_CUSTOM_API_KEY"
@@ -132,7 +132,7 @@ You can extend Aider's capabilities by adding custom extensions:
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
@@ -211,7 +211,7 @@ data "coder_parameter" "ai_prompt" {
module "aider" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/aider/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
ai_api_key = var.anthropic_api_key
task_prompt = data.coder_parameter.ai_prompt.value
+33 -40
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -24,12 +24,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
@@ -230,17 +224,17 @@ resource "coder_script" "aider" {
}
echo "Setting up Aider AI pair programming..."
if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then
echo "Error: Both use_screen and use_tmux cannot be enabled at the same time."
exit 1
fi
mkdir -p "${var.folder}"
if [ "$(uname)" = "Linux" ]; then
echo "Checking dependencies for Linux..."
if [ "${var.use_tmux}" = "true" ]; then
if ! command_exists tmux; then
echo "Installing tmux for persistent sessions..."
@@ -302,7 +296,7 @@ resource "coder_script" "aider" {
if [ "${var.install_aider}" = "true" ]; then
echo "Installing Aider..."
if ! command_exists python3 || ! command_exists pip3; then
echo "Installing Python dependencies required for Aider..."
if command -v apt-get >/dev/null 2>&1; then
@@ -325,37 +319,37 @@ resource "coder_script" "aider" {
else
echo "Python is already installed, skipping installation."
fi
if ! command_exists aider; then
curl -LsSf https://aider.chat/install.sh | sh
fi
if [ -f "$HOME/.bashrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc"
fi
fi
if [ -f "$HOME/.zshrc" ]; then
if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then
echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc"
fi
fi
fi
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh
chmod +x /tmp/post_install.sh
/tmp/post_install.sh
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Aider to report tasks via Coder MCP..."
mkdir -p "$HOME/.config/aider"
cat > "$HOME/.config/aider/config.yml" << EOL
${trimspace(local.combined_extensions)}
EOL
@@ -363,29 +357,29 @@ EOL
fi
echo "Starting persistent Aider session..."
touch "$HOME/.aider.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
export PATH="$HOME/bin:$PATH"
if [ "${var.use_tmux}" = "true" ]; then
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in tmux session..."
# Configure tmux for shared sessions
if [ ! -f "$HOME/.tmux.conf" ]; then
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\""
echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress."
@@ -395,12 +389,12 @@ EOL
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\""
echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button."
@@ -408,12 +402,12 @@ EOL
else
if [ -n "${var.task_prompt}" ]; then
echo "Running Aider with message in screen session..."
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
@@ -423,7 +417,7 @@ EOL
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
@@ -432,15 +426,15 @@ EOL
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
/bin/bash
"
echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress."
else
if [ ! -f "$HOME/.screenrc" ]; then
echo "Creating ~/.screenrc and adding multiuser settings..."
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..."
echo "multiuser on" >> "$HOME/.screenrc"
@@ -450,7 +444,7 @@ EOL
echo "Adding 'acladd $(whoami)' to ~/.screenrc..."
echo "acladd $(whoami)" >> "$HOME/.screenrc"
fi
echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}"
screen -U -dmS ${var.session_name} bash -c "
cd ${var.folder}
@@ -462,7 +456,7 @@ EOL
echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button."
fi
fi
echo "Aider setup complete!"
EOT
run_on_start = true
@@ -477,12 +471,12 @@ resource "coder_app" "aider_cli" {
command = <<-EOT
#!/bin/bash
set -e
export PATH="$HOME/bin:$HOME/.local/bin:$PATH"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.use_tmux}" = "true" ]; then
if tmux has-session -t ${var.session_name} 2>/dev/null; then
echo "Attaching to existing Aider tmux session..."
@@ -505,5 +499,4 @@ resource "coder_app" "aider_cli" {
fi
EOT
order = var.order
group = var.group
}
@@ -19,7 +19,7 @@ Enable DCV Server and Web Client on Windows workspaces.
module "dcv" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/amazon-dcv-windows/coder"
version = "1.1.0"
version = "1.0.24"
agent_id = resource.coder_agent.main.id
}
@@ -4,23 +4,11 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
@@ -57,8 +45,6 @@ resource "coder_app" "web-dcv" {
url = "https://localhost:${var.port}${local.web_url_path}?username=${local.admin_username}&password=${var.admin_password}"
icon = "/icon/dcv.svg"
subdomain = var.subdomain
order = var.order
group = var.group
}
resource "coder_script" "install-dcv" {
+4 -4
View File
@@ -14,7 +14,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
# Required: see below for how to generate
experiment_auth_tarball = var.amazon_q_auth_tarball
@@ -82,7 +82,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_use_tmux = true
@@ -94,7 +94,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_report_tasks = true
@@ -106,7 +106,7 @@ module "amazon-q" {
```tf
module "amazon-q" {
source = "registry.coder.com/coder/amazon-q/coder"
version = "1.1.0"
version = "1.0.1"
agent_id = coder_agent.example.id
experiment_auth_tarball = var.amazon_q_auth_tarball
experiment_pre_install_script = "echo Pre-install!"
+9 -17
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -24,12 +24,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
@@ -219,7 +213,7 @@ resource "coder_script" "amazon_q" {
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Amazon Q to report tasks via Coder MCP..."
echo "Configuring Amazon Q to report tasks via Coder MCP..."
mkdir -p ~/.aws/amazonq
echo "${local.encoded_mcp_json}" | base64 -d > ~/.aws/amazonq/mcp.json
echo "Created the ~/.aws/amazonq/mcp.json configuration file"
@@ -233,19 +227,19 @@ resource "coder_script" "amazon_q" {
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Amazon Q in the background with tmux..."
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.amazon-q.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
tmux new-session -d -s amazon-q -c "${var.folder}" "q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log" && exec bash"
tmux send-keys -t amazon-q "${local.full_prompt}"
sleep 5
tmux send-keys -t amazon-q Enter
@@ -253,7 +247,7 @@ resource "coder_script" "amazon_q" {
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Amazon Q in the background..."
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
exit 1
@@ -265,7 +259,7 @@ resource "coder_script" "amazon_q" {
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.amazon-q.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.amazon-q.log"
echo "multiuser on" >> "$HOME/.screenrc"
@@ -277,7 +271,7 @@ resource "coder_script" "amazon_q" {
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS amazon-q bash -c '
cd ${var.folder}
q chat --trust-all-tools | tee -a "$HOME/.amazon-q.log
@@ -332,6 +326,4 @@ resource "coder_app" "amazon_q" {
fi
EOT
icon = var.icon
order = var.order
group = var.group
}
+3 -8
View File
@@ -14,7 +14,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "1.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@@ -22,11 +22,6 @@ module "claude-code" {
}
```
> **Security Notice**: This module uses the [`--dangerously-skip-permissions`](https://docs.anthropic.com/en/docs/claude-code/cli-usage#cli-flags) flag when running Claude Code. This flag
> bypasses standard permission checks and allows Claude Code broader access to your system than normally permitted. While
> this enables more functionality, it also means Claude Code can potentially execute commands with the same privileges as
> the user running it. Use this module _only_ in trusted environments and be aware of the security implications.
## Prerequisites
- Node.js and npm must be installed in your workspace to install Claude Code
@@ -88,7 +83,7 @@ resource "coder_agent" "main" {
module "claude-code" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
@@ -107,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "1.3.1"
version = "1.2.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_claude_code = true
+27 -33
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -24,12 +24,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
@@ -103,16 +97,6 @@ resource "coder_script" "claude_code" {
command -v "$1" >/dev/null 2>&1
}
# Check if the specified folder exists
if [ ! -d "${var.folder}" ]; then
echo "Warning: The specified folder '${var.folder}' does not exist."
echo "Creating the folder..."
# The folder must exist before tmux is started or else claude will start
# in the home directory.
mkdir -p "${var.folder}"
echo "Folder created successfully."
fi
# Run pre-install script if provided
if [ -n "${local.encoded_pre_install_script}" ]; then
echo "Running pre-install script..."
@@ -131,11 +115,6 @@ resource "coder_script" "claude_code" {
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
coder exp mcp configure claude-code ${var.folder}
fi
# Run post-install script if provided
if [ -n "${local.encoded_post_install_script}" ]; then
echo "Running post-install script..."
@@ -144,6 +123,11 @@ resource "coder_script" "claude_code" {
/tmp/post_install.sh
fi
if [ "${var.experiment_report_tasks}" = "true" ]; then
echo "Configuring Claude Code to report tasks via Coder MCP..."
coder exp mcp configure claude-code ${var.folder}
fi
# Handle terminal multiplexer selection (tmux or screen)
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
@@ -154,7 +138,7 @@ resource "coder_script" "claude_code" {
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Claude Code in the background with tmux..."
# Check if tmux is installed
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
@@ -162,19 +146,25 @@ resource "coder_script" "claude_code" {
fi
touch "$HOME/.claude-code.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Create a new tmux session in detached mode
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions"
# Send the prompt to the tmux session if needed
if [ -n "$CODER_MCP_CLAUDE_TASK_PROMPT" ]; then
tmux send-keys -t claude-code "$CODER_MCP_CLAUDE_TASK_PROMPT"
sleep 5
tmux send-keys -t claude-code Enter
fi
fi
# Run with screen if enabled
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Claude Code in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
@@ -188,7 +178,7 @@ resource "coder_script" "claude_code" {
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
echo "multiuser on" >> "$HOME/.screenrc"
@@ -200,12 +190,18 @@ resource "coder_script" "claude_code" {
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS claude-code bash -c '
cd ${var.folder}
claude --dangerously-skip-permissions "$CODER_MCP_CLAUDE_TASK_PROMPT" | tee -a "$HOME/.claude-code.log"
claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"
exec bash
'
# Extremely hacky way to send the prompt to the screen session
# This will be fixed in the future, but `claude` was not sending MCP
# tasks when an initial prompt is provided.
screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT"
sleep 5
screen -S claude-code -X stuff "^M"
else
# Check if claude is installed before running
if ! command_exists claude; then
@@ -250,6 +246,4 @@ resource "coder_app" "claude_code" {
fi
EOT
icon = var.icon
order = var.order
group = var.group
}
+7 -7
View File
@@ -15,7 +15,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "code-server" {
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
install_version = "4.8.3"
}
@@ -44,7 +44,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -62,7 +62,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -79,7 +79,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@@ -95,7 +95,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@@ -108,7 +108,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
version = "1.3.0"
version = "1.2.0"
agent_id = coder_agent.example.id
offline = true
}
+1 -8
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 2.1"
}
}
}
@@ -89,12 +89,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "offline" {
type = bool
description = "Just run code-server in the background, don't fetch it from GitHub"
@@ -193,7 +187,6 @@ resource "coder_app" "code-server" {
subdomain = var.subdomain
share = var.share
order = var.order
group = var.group
open_in = var.open_in
healthcheck {
+2 -2
View File
@@ -17,7 +17,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.0"
version = "1.1.0"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "cursor" {
module "cursor" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/cursor/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
+1 -8
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.23"
}
}
}
@@ -32,12 +32,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "slug" {
type = string
description = "The slug of the app."
@@ -60,7 +54,6 @@ resource "coder_app" "cursor" {
slug = var.slug
display_name = var.display_name
order = var.order
group = var.group
url = join("", [
"cursor://coder.coder-remote/open",
"?owner=",
+6 -6
View File
@@ -19,7 +19,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@@ -32,7 +32,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
}
```
@@ -43,7 +43,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
user = "root"
}
@@ -55,14 +55,14 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
@@ -77,7 +77,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.1.0"
version = "1.0.29"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
+1 -15
View File
@@ -4,23 +4,11 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.12"
}
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
@@ -91,8 +79,6 @@ resource "coder_app" "dotfiles" {
display_name = "Refresh Dotfiles"
slug = "dotfiles"
icon = "/icon/dotfiles.svg"
order = var.order
group = var.group
command = templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user
+4 -4
View File
@@ -15,7 +15,7 @@ A file browser for your workspace.
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -42,7 +42,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
database_path = ".config/filebrowser.db"
}
@@ -54,7 +54,7 @@ module "filebrowser" {
module "filebrowser" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/filebrowser/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
agent_name = "main"
subdomain = false
+2 -9
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -68,12 +68,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "slug" {
type = string
description = "The slug of the coder_app resource."
@@ -114,7 +108,6 @@ resource "coder_app" "filebrowser" {
subdomain = var.subdomain
share = var.share
order = var.order
group = var.group
healthcheck {
url = local.healthcheck_url
@@ -127,4 +120,4 @@ locals {
server_base_path = var.subdomain ? "" : format("/@%s/%s%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name != null ? ".${var.agent_name}" : "", var.slug)
url = "http://localhost:${var.port}${local.server_base_path}"
healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health"
}
}
+7 -11
View File
@@ -14,7 +14,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 = "1.3.0"
version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -24,14 +24,14 @@ module "goose" {
## Prerequisites
- `screen` or `tmux` must be installed in your workspace to run Goose in the background
- `screen` must be installed in your workspace to run Goose in the background
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces.
## Examples
Your workspace must have `screen` or `tmux` installed to use the background session functionality.
Your workspace must have `screen` installed to use this.
### Run in the background and report tasks (Experimental)
@@ -90,7 +90,7 @@ resource "coder_agent" "main" {
module "goose" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/goose/coder"
version = "1.3.0"
version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
@@ -99,12 +99,8 @@ module "goose" {
# Enable experimental features
experiment_report_tasks = true
# Run Goose in the background with screen (pick one: screen or tmux)
# Run Goose in the background
experiment_use_screen = true
# experiment_use_tmux = true # Alternative: use tmux instead of screen
# Optional: customize the session name (defaults to "goose")
# session_name = "goose-session"
# Avoid configuring Goose manually
experiment_auto_configure = true
@@ -147,12 +143,12 @@ Note: The indentation in the heredoc is preserved, so you can write the YAML nat
## Run standalone
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or tmux, and without any task reporting to the Coder UI.
Run Goose as a standalone app in your workspace. This will install Goose and run it directly without using screen or any task reporting to the Coder UI.
```tf
module "goose" {
source = "registry.coder.com/coder/goose/coder"
version = "1.3.0"
version = "1.1.1"
agent_id = coder_agent.example.id
folder = "/home/coder"
install_goose = true
+34 -87
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -24,12 +24,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "icon" {
type = string
description = "The icon to use for the app."
@@ -60,18 +54,6 @@ variable "experiment_use_screen" {
default = false
}
variable "experiment_use_tmux" {
type = bool
description = "Whether to use tmux instead of screen for running Goose in the background."
default = false
}
variable "session_name" {
type = string
description = "Name for the persistent session (screen or tmux)"
default = "goose"
}
variable "experiment_report_tasks" {
type = bool
description = "Whether to enable task reporting."
@@ -200,59 +182,15 @@ GOOSE_MODEL: ${var.experiment_goose_model}
${trimspace(local.combined_extensions)}
EOL
fi
# Write system prompt to config
mkdir -p "$HOME/.config/goose"
echo "$GOOSE_SYSTEM_PROMPT" > "$HOME/.config/goose/.goosehints"
# Handle terminal multiplexer selection (tmux or screen)
if [ "${var.experiment_use_tmux}" = "true" ] && [ "${var.experiment_use_screen}" = "true" ]; then
echo "Error: Both experiment_use_tmux and experiment_use_screen cannot be true simultaneously."
echo "Please set only one of them to true."
exit 1
fi
# Determine goose command
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
# Run with tmux if enabled
if [ "${var.experiment_use_tmux}" = "true" ]; then
echo "Running Goose in the background with tmux..."
# Check if tmux is installed
if ! command_exists tmux; then
echo "Error: tmux is not installed. Please install tmux manually."
exit 1
fi
touch "$HOME/.goose.log"
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# Configure tmux for shared sessions
if [ ! -f "$HOME/.tmux.conf" ]; then
echo "Creating ~/.tmux.conf with shared session settings..."
echo "set -g mouse on" > "$HOME/.tmux.conf"
fi
if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then
echo "Adding 'set -g mouse on' to ~/.tmux.conf..."
echo "set -g mouse on" >> "$HOME/.tmux.conf"
fi
# Create a new tmux session in detached mode
tmux new-session -d -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
elif [ "${var.experiment_use_screen}" = "true" ]; then
# Run with screen if enabled
if [ "${var.experiment_use_screen}" = "true" ]; then
echo "Running Goose in the background..."
# Check if screen is installed
if ! command_exists screen; then
echo "Error: screen is not installed. Please install screen manually."
@@ -266,7 +204,7 @@ EOL
echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.goose.log"
echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc"
fi
if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then
echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.goose.log"
echo "multiuser on" >> "$HOME/.screenrc"
@@ -278,12 +216,32 @@ EOL
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
screen -U -dmS ${var.session_name} bash -c "
# Determine goose command
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
screen -U -dmS goose bash -c "
cd ${var.folder}
\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"
/bin/bash
"
else
# Check if goose is installed before running
if command_exists goose; then
GOOSE_CMD=goose
elif [ -f "$HOME/.local/bin/goose" ]; then
GOOSE_CMD="$HOME/.local/bin/goose"
else
echo "Error: Goose is not installed. Please enable install_goose or install it manually."
exit 1
fi
fi
EOT
run_on_start = true
@@ -312,31 +270,20 @@ resource "coder_app" "goose" {
exit 1
fi
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
if [ "${var.experiment_use_tmux}" = "true" ]; then
if tmux has-session -t ${var.session_name} 2>/dev/null; then
echo "Attaching to existing Goose tmux session." | tee -a "$HOME/.goose.log"
tmux attach-session -t ${var.session_name}
else
echo "Starting a new Goose tmux session." | tee -a "$HOME/.goose.log"
tmux new-session -s ${var.session_name} -c ${var.folder} "\"$GOOSE_CMD\" run --text \"Review your goosehints. Every step of the way, report tasks to Coder with proper descriptions and statuses. Your task at hand: $GOOSE_TASK_PROMPT\" --interactive | tee -a \"$HOME/.goose.log\"; exec bash"
fi
elif [ "${var.experiment_use_screen}" = "true" ]; then
if [ "${var.experiment_use_screen}" = "true" ]; then
# Check if session exists first
if ! screen -list | grep -q "${var.session_name}"; then
if ! screen -list | grep -q "goose"; then
echo "Error: No existing Goose session found. Please wait for the script to start it."
exit 1
fi
# Only attach to existing session
screen -xRR ${var.session_name}
screen -xRR goose
else
cd ${var.folder}
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
"$GOOSE_CMD" run --text "Review goosehints. Your task: $GOOSE_TASK_PROMPT" --interactive
fi
EOT
icon = var.icon
order = var.order
group = var.group
}
@@ -18,7 +18,7 @@ Consult the [JetBrains documentation](https://www.jetbrains.com/help/idea/prereq
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
@@ -36,7 +36,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -50,7 +50,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -65,7 +65,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["IU", "PY"]
@@ -90,7 +90,7 @@ module "jetbrains_gateway" {
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -108,7 +108,7 @@ Due to the highest priority of the `ide_download_link` parameter in the `(jetbra
module "jetbrains_gateway" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jetbrains-gateway/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/example"
jetbrains_ides = ["GO", "WS"]
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
http = {
source = "hashicorp/http"
@@ -45,7 +45,7 @@ variable "folder" {
type = string
description = "The directory to open in the IDE. e.g. /home/coder/project"
validation {
condition = can(regex("^(?:/[^/]+)+/?$", var.folder))
condition = can(regex("^(?:/[^/]+)+$", var.folder))
error_message = "The folder must be a full path and must not start with a ~."
}
}
@@ -62,12 +62,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "coder_parameter_order" {
type = number
description = "The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order)."
@@ -330,7 +324,6 @@ resource "coder_app" "gateway" {
icon = local.icon
external = true
order = var.order
group = var.group
url = join("", [
"jetbrains-gateway://connect#type=coder&workspace=",
data.coder_workspace.me.name,
@@ -17,7 +17,7 @@ A module that adds Jupyter Notebook in your Coder template.
module "jupyter-notebook" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jupyter-notebook/coder"
version = "1.1.0"
version = "1.0.19"
agent_id = coder_agent.example.id
}
```
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -42,12 +42,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "jupyter-notebook" {
agent_id = var.agent_id
display_name = "jupyter-notebook"
@@ -68,5 +62,4 @@ resource "coder_app" "jupyter-notebook" {
subdomain = true
share = var.share
order = var.order
group = var.group
}
+1 -1
View File
@@ -17,7 +17,7 @@ A module that adds JupyterLab in your Coder template.
module "jupyterlab" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jupyterlab/coder"
version = "1.1.0"
version = "1.0.31"
agent_id = coder_agent.example.id
}
```
+1 -8
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -51,12 +51,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
resource "coder_script" "jupyterlab" {
agent_id = var.agent_id
display_name = "jupyterlab"
@@ -78,5 +72,4 @@ resource "coder_app" "jupyterlab" {
subdomain = var.subdomain
share = var.share
order = var.order
group = var.group
}
+1 -2
View File
@@ -15,10 +15,9 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and
module "kasmvnc" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/kasmvnc/coder"
version = "1.2.0"
version = "1.0.23"
agent_id = coder_agent.example.id
desktop_environment = "xfce"
subdomain = true
}
```
+7 -31
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.12"
}
}
}
@@ -29,56 +29,32 @@ variable "kasm_version" {
variable "desktop_environment" {
type = string
description = "Specifies the desktop environment of the workspace. This should be pre-installed on the workspace."
validation {
condition = contains(["xfce", "kde", "gnome", "lxde", "lxqt"], var.desktop_environment)
error_message = "Invalid desktop environment. Please specify a valid desktop environment."
}
}
variable "order" {
type = number
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "subdomain" {
type = bool
default = true
description = "Is subdomain sharing enabled in your cluster?"
}
resource "coder_script" "kasm_vnc" {
agent_id = var.agent_id
display_name = "KasmVNC"
icon = "/icon/kasmvnc.svg"
run_on_start = true
script = templatefile("${path.module}/run.sh", {
PORT = var.port,
DESKTOP_ENVIRONMENT = var.desktop_environment,
KASM_VERSION = var.kasm_version
SUBDOMAIN = tostring(var.subdomain)
PATH_VNC_HTML = var.subdomain ? "" : file("${path.module}/path_vnc.html")
PORT : var.port,
DESKTOP_ENVIRONMENT : var.desktop_environment,
KASM_VERSION : var.kasm_version
})
run_on_start = true
}
resource "coder_app" "kasm_vnc" {
agent_id = var.agent_id
slug = "kasm-vnc"
display_name = "KasmVNC"
display_name = "kasmVNC"
url = "http://localhost:${var.port}"
icon = "/icon/kasmvnc.svg"
subdomain = var.subdomain
subdomain = true
share = "owner"
order = var.order
group = var.group
healthcheck {
url = "http://localhost:${var.port}/app"
interval = 5
@@ -1,81 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Path-Sharing Bounce Page</title>
<style type="text/css">
:root {
color-scheme: light dark;
--dark: #121212;
--header-bg: rgba(127,127,127,0.2);
--light: white;
--rule-color: light-dark(rgba(0,0,0,0.8), rgba(255,255,255,0.8));
background-color: light-dark(var(--light), var(--dark));
color: light-dark(var(--dark), var(--light));
}
body, h1, p {
box-sizing: border-box;
margin:0; padding:0;
}
body{
font-family:Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
h1{
width: 100%;
padding: 1rem;
letter-spacing: -1.5pt;
padding-bottom:10px;
border-bottom: 1px solid var(--rule-color);
background-color: var(--header-bg);
}
p {
padding: 1rem; letter-spacing: -0.5pt;}
a.indent { display:inline-block; padding-top:0.5rem; padding-left: 2rem; font-size:0.8rem }
</style>
<meta charset="UTF-8" />
</head>
<body>
<h1>Path-Sharing Bounce Page</h1>
<p>
This application is being served via path sharing.
If you are not redirected, <span id="help">check the
Javascript console in your browser's developer tools
for more information.</span>
</p>
</body>
<script language="javascript">
// This page exists to satisfy the querystring driven client API
// specified here - https://raw.githubusercontent.com/kasmtech/noVNC/bce2d6a7048025c6e6c05df9d98b206c23f6dbab/docs/EMBEDDING.md
// tl;dr:
// * `host` - The WebSocket host to connect to.
// This is just the hostname component of the original URL
// * `port` - The WebSocket port to connect to.
// It doesn't look like we need to set this unless it's different
// than the incoming http request.
// * `encrypt` - If TLS should be used for the WebSocket connection.
// we base this on whether or not the protocol is `https`, seems
// reasonable for now.
// * `path` - The WebSocket path to use.
// This apparently doesn't tolerate a leading `/` so we use a
// function to tidy that up.
function trimFirstCharIf(str, char) {
return str.charAt(0) === char ? str.slice(1) : str;
}
function trimLastCharIf(str, char) {
return str.endsWith("/") ? str.slice(0,str.length-1) : str;
}
const newloc = new URL(window.location);
const h = document.getElementById("help")
// Building the websockify path must happen before we append the filename to newloc.pathname
newloc.searchParams.append("path",
trimLastCharIf(trimFirstCharIf(newloc.pathname,"/"),"/")+"/websockify");
newloc.searchParams.append("encrypted", newloc.protocol==="https:"? true : false);
newloc.pathname += "vnc.html"
console.log(newloc);
h.innerHTML = `click <a id="link" href="${newloc.toString()}">here</a> to go to the application.
<br/><br/>The rewritten URL is:<br/><a id="link" class="indent" href="${newloc.toString()}">${newloc.toString()}</a>`
window.location = newloc.href;
</script>
</html>
+8 -77
View File
@@ -3,8 +3,6 @@
# Exit on error, undefined variables, and pipe failures
set -euo pipefail
error() { printf "💀 ERROR: %s\n" "$@"; exit 1; }
# Function to check if vncserver is already installed
check_installed() {
if command -v vncserver &> /dev/null; then
@@ -190,7 +188,7 @@ if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then
SUDO=sudo
else
kasm_config_file="$HOME/.vnc/kasmvnc.yaml"
SUDO=""
SUDO=
echo "WARNING: Sudo access not available, using user config dir!"
@@ -208,7 +206,6 @@ echo "Writing KasmVNC config to $kasm_config_file"
$SUDO tee "$kasm_config_file" > /dev/null << EOF
network:
protocol: http
interface: 127.0.0.1
websocket_port: ${PORT}
ssl:
require_ssl: false
@@ -223,82 +220,16 @@ EOF
# and does not listen publicly
echo -e "password\npassword\n" | vncpasswd -wo -u "$USER"
get_http_dir() {
# determine the served file path
# Start with the default
httpd_directory="/usr/share/kasmvnc/www"
# Check the system configuration path
if [[ -e /etc/kasmvnc/kasmvnc.yaml ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
# If this grep is successful, it will return:
# httpd_directory: /usr/share/kasmvnc/www
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
fi
# Check the home directory for overriding values
if [[ -e "$HOME/.vnc/kasmvnc.yaml" ]]; then
d=($(grep -E "^\s*httpd_directory:.*$" /etc/kasmvnc/kasmvnc.yaml))
if [[ $${#d[@]} -eq 2 && -d "$${d[1]}" ]]; then
httpd_directory="$${d[1]}"
fi
fi
echo $httpd_directory
}
fix_server_index_file(){
local fname=$${FUNCNAME[0]} # gets current function name
if [[ $# -ne 1 ]]; then
error "$fname requires exactly 1 parameter:\n\tpath to KasmVNC httpd_directory"
fi
local httpdir="$1"
if [[ ! -d "$httpdir" ]]; then
error "$fname: $httpdir is not a directory"
fi
pushd "$httpdir" > /dev/null
cat <<'EOH' > /tmp/path_vnc.html
${PATH_VNC_HTML}
EOH
$SUDO mv /tmp/path_vnc.html .
# check for the switcheroo
if [[ -f "index.html" && -L "vnc.html" ]]; then
$SUDO mv $httpdir/index.html $httpdir/vnc.html
fi
$SUDO ln -s -f path_vnc.html index.html
popd > /dev/null
}
patch_kasm_http_files(){
homedir=$(get_http_dir)
fix_server_index_file "$homedir"
}
if [[ "${SUBDOMAIN}" == "false" ]]; then
echo "🩹 Patching up webserver files to support path-sharing..."
patch_kasm_http_files
fi
VNC_LOG="/tmp/kasmvncserver.log"
# Start the server
printf "🚀 Starting KasmVNC server...\n"
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > /tmp/kasmvncserver.log 2>&1 &
pid=$!
set +e
vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1
RETVAL=$?
set -e
if [[ $RETVAL -ne 0 ]]; then
echo "ERROR: Failed to start KasmVNC server. Return code: $RETVAL"
if [[ -f "$VNC_LOG" ]]; then
echo "Full logs:"
cat "$VNC_LOG"
else
echo "ERROR: Log file not found: $VNC_LOG"
fi
# Wait for server to start
sleep 5
grep -v '^[[:space:]]*$' /tmp/kasmvncserver.log | tail -n 10
if ps -p $pid | grep -q "^$pid"; then
echo "ERROR: Failed to start KasmVNC server. Check full logs at /tmp/kasmvncserver.log"
exit 1
fi
printf "🚀 KasmVNC server started successfully!\n"
+6 -7
View File
@@ -20,12 +20,11 @@ variable "vault_token" {
}
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.2.0"
agent_id = coder_agent.example.id
vault_token = var.token # optional
vault_addr = "https://vault.example.com"
vault_namespace = "prod" # optional, vault enterprise only
source = "registry.coder.com/coder/vault-token/coder"
version = "1.1.0"
agent_id = coder_agent.example.id
vault_token = var.token # optional
vault_addr = "https://vault.example.com"
}
```
@@ -75,7 +74,7 @@ variable "vault_token" {
module "vault" {
source = "registry.coder.com/coder/vault-token/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
vault_addr = "https://vault.example.com"
vault_token = var.token
@@ -26,11 +26,6 @@ variable "vault_token" {
sensitive = true
default = null
}
variable "vault_namespace" {
type = string
description = "The Vault namespace to use."
default = null
}
variable "vault_cli_version" {
type = string
@@ -67,10 +62,3 @@ resource "coder_env" "vault_token" {
name = "VAULT_TOKEN"
value = var.vault_token
}
resource "coder_env" "vault_namespace" {
count = var.vault_namespace != null ? 1 : 0
agent_id = var.agent_id
name = "VAULT_NAMESPACE"
value = var.vault_namespace
}
@@ -17,7 +17,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
version = "1.0.15"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "vscode" {
module "vscode" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-desktop/coder"
version = "1.1.0"
version = "1.0.15"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.23"
}
}
}
@@ -32,12 +32,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
@@ -48,8 +42,6 @@ resource "coder_app" "vscode" {
slug = "vscode"
display_name = "VS Code Desktop"
order = var.order
group = var.group
url = join("", [
"vscode://coder.coder-remote/open",
"?owner=",
+5 -5
View File
@@ -15,7 +15,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
accept_license = true
}
@@ -31,7 +31,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
install_prefix = "/home/coder/.vscode-web"
folder = "/home/coder"
@@ -45,7 +45,7 @@ module "vscode-web" {
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
accept_license = true
@@ -60,7 +60,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -78,7 +78,7 @@ By default, this module installs the latest. To pin a specific version, retrieve
module "vscode-web" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/vscode-web/coder"
version = "1.2.0"
version = "1.1.0"
agent_id = coder_agent.example.id
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
accept_license = true
+1 -8
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.17"
}
}
}
@@ -97,12 +97,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
variable "settings" {
type = any
description = "A map of settings to apply to VS Code web."
@@ -200,7 +194,6 @@ resource "coder_app" "vscode-web" {
subdomain = var.subdomain
share = var.share
order = var.order
group = var.group
healthcheck {
url = local.healthcheck_url
Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 99 KiB

+2 -2
View File
@@ -17,7 +17,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
version = "1.0.0"
agent_id = coder_agent.example.id
}
```
@@ -30,7 +30,7 @@ module "windsurf" {
module "windsurf" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/windsurf/coder"
version = "1.1.0"
version = "1.0.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"
}
+1 -8
View File
@@ -4,7 +4,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
version = ">= 0.23"
}
}
}
@@ -32,12 +32,6 @@ variable "order" {
default = null
}
variable "group" {
type = string
description = "The name of a group that this app belongs to."
default = null
}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}
@@ -48,7 +42,6 @@ resource "coder_app" "windsurf" {
slug = "windsurf"
display_name = "Windsurf Editor"
order = var.order
group = var.group
url = join("", [
"windsurf://coder.coder-remote/open",
"?owner=",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

-1
View File
@@ -2,7 +2,6 @@
display_name: Nataindata
bio: Data engineer
github: nataindata
avatar: ./.images/avatar.png
website: https://www.nataindata.com
status: community
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

+1 -2
View File
@@ -1,8 +1,7 @@
---
display_name: TheZoker
bio: I'm a master computer science student at the TU munich and a web designer.
bio: I'm a master computer science student at the TU munich and a webdesigner.
github: TheZoker
avatar: ./.images/avatar.jpeg
website: https://gareis.io/
status: community
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 27 KiB

-1
View File
@@ -1,7 +1,6 @@
---
display_name: WhizUs
bio: WhizUs is your premier choice for DevOps, Kubernetes, and Cloud Native consulting. Based in Vienna we combine our expert solutions with a strong commitment to the community. Explore automation, scalability and drive success through collaboration.
avatar: ./.images/avatar.png
github: WhizUs
linkedin: https://www.linkedin.com/company/whizus
website: https://www.whizus.com/
-255
View File
@@ -1,255 +0,0 @@
// Package gorules defines custom lint rules for ruleguard.
//
// golangci-lint runs these rules via go-critic, which includes support
// for ruleguard. All Go files in this directory define lint rules
// in the Ruleguard DSL; see:
//
// - https://go-ruleguard.github.io/by-example/
// - https://pkg.go.dev/github.com/quasilyte/go-ruleguard/dsl
//
// You run one of the following commands to execute your go rules only:
//
// golangci-lint run
// golangci-lint run --disable-all --enable=gocritic
//
// Note: don't forget to run `golangci-lint cache clean`!
package gorules
import (
"github.com/quasilyte/go-ruleguard/dsl"
)
// Use xerrors everywhere! It provides additional stacktrace info!
//
//nolint:unused,deadcode,varnamelen
func xerrors(m dsl.Matcher) {
m.Import("errors")
m.Import("fmt")
m.Import("golang.org/x/xerrors")
m.Match("fmt.Errorf($arg)").
Suggest("xerrors.New($arg)").
Report("Use xerrors to provide additional stacktrace information!")
m.Match("fmt.Errorf($arg1, $*args)").
Suggest("xerrors.Errorf($arg1, $args)").
Report("Use xerrors to provide additional stacktrace information!")
m.Match("errors.$_($msg)").
Where(m["msg"].Type.Is("string")).
Suggest("xerrors.New($msg)").
Report("Use xerrors to provide additional stacktrace information!")
}
// databaseImport enforces not importing any database types into /codersdk.
//
//nolint:unused,deadcode,varnamelen
func databaseImport(m dsl.Matcher) {
m.Import("github.com/coder/coder/v2/coderd/database")
m.Match("database.$_").
Report("Do not import any database types into codersdk").
Where(m.File().PkgPath.Matches("github.com/coder/coder/v2/codersdk"))
}
// doNotCallTFailNowInsideGoroutine enforces not calling t.FailNow or
// functions that may themselves call t.FailNow in goroutines outside
// the main test goroutine. See testing.go:834 for why.
//
//nolint:unused,deadcode,varnamelen
func doNotCallTFailNowInsideGoroutine(m dsl.Matcher) {
m.Import("testing")
m.Match(`
go func($*_){
$*_
$require.$_($*_)
$*_
}($*_)`).
At(m["require"]).
Where(m["require"].Text == "require").
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
// require.Eventually runs the function in a goroutine.
m.Match(`
require.Eventually(t, func() bool {
$*_
$require.$_($*_)
$*_
}, $*_)`).
At(m["require"]).
Where(m["require"].Text == "require").
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
m.Match(`
go func($*_){
$*_
$t.$fail($*_)
$*_
}($*_)`).
At(m["fail"]).
Where(m["t"].Type.Implements("testing.TB") && m["fail"].Text.Matches("^(FailNow|Fatal|Fatalf)$")).
Report("Do not call functions that may call t.FailNow in a goroutine, as this can cause data races (see testing.go:834)")
}
// useStandardTimeoutsAndDelaysInTests ensures all tests use common
// constants for timeouts and delays in usual scenarios, this allows us
// to tweak them based on platform (important to avoid CI flakes).
//
//nolint:unused,deadcode,varnamelen
func useStandardTimeoutsAndDelaysInTests(m dsl.Matcher) {
m.Import("github.com/stretchr/testify/require")
m.Import("github.com/stretchr/testify/assert")
m.Import("github.com/coder/coder/v2/testutil")
m.Match(`context.WithTimeout($ctx, $duration)`).
Where(m.File().Imports("testing") && !m.File().PkgPath.Matches("testutil$") && !m["duration"].Text.Matches("^testutil\\.")).
At(m["duration"]).
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
m.Match(`
$testify.$Eventually($t, func() bool {
$*_
}, $timeout, $interval, $*_)
`).
Where((m["testify"].Text == "require" || m["testify"].Text == "assert") &&
(m["Eventually"].Text == "Eventually" || m["Eventually"].Text == "Eventuallyf") &&
!m["timeout"].Text.Matches("^testutil\\.")).
At(m["timeout"]).
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
m.Match(`
$testify.$Eventually($t, func() bool {
$*_
}, $timeout, $interval, $*_)
`).
Where((m["testify"].Text == "require" || m["testify"].Text == "assert") &&
(m["Eventually"].Text == "Eventually" || m["Eventually"].Text == "Eventuallyf") &&
!m["interval"].Text.Matches("^testutil\\.")).
At(m["interval"]).
Report("Do not use magic numbers in test timeouts and delays. Use the standard testutil.Wait* or testutil.Interval* constants instead.")
}
// ProperRBACReturn ensures we always write to the response writer after a
// call to Authorize. If we just do a return, the client will get a status code
// 200, which is incorrect.
func ProperRBACReturn(m dsl.Matcher) {
m.Match(`
if !$_.Authorize($*_) {
return
}
`).Report("Must write to 'ResponseWriter' before returning'")
}
// slogFieldNameSnakeCase is a lint rule that ensures naming consistency
// of logged field names.
func slogFieldNameSnakeCase(m dsl.Matcher) {
m.Import("cdr.dev/slog")
m.Match(
`slog.F($name, $value)`,
).
Where(m["name"].Const && !m["name"].Text.Matches(`^"[a-z]+(_[a-z]+)*"$`)).
Report("Field name $name must be snake_case.")
}
// slogUUIDFieldNameHasIDSuffix ensures that "uuid.UUID" field has ID prefix
// in the field name.
func slogUUIDFieldNameHasIDSuffix(m dsl.Matcher) {
m.Import("cdr.dev/slog")
m.Import("github.com/google/uuid")
m.Match(
`slog.F($name, $value)`,
).
Where(m["value"].Type.Is("uuid.UUID") && !m["name"].Text.Matches(`_id"$`)).
Report(`uuid.UUID field $name must have "_id" suffix.`)
}
// slogMessageFormat ensures that the log message starts with lowercase, and does not
// end with special character.
func slogMessageFormat(m dsl.Matcher) {
m.Import("cdr.dev/slog")
m.Match(
`logger.Error($ctx, $message, $*args)`,
`logger.Warn($ctx, $message, $*args)`,
`logger.Info($ctx, $message, $*args)`,
`logger.Debug($ctx, $message, $*args)`,
`$foo.logger.Error($ctx, $message, $*args)`,
`$foo.logger.Warn($ctx, $message, $*args)`,
`$foo.logger.Info($ctx, $message, $*args)`,
`$foo.logger.Debug($ctx, $message, $*args)`,
`Logger.Error($ctx, $message, $*args)`,
`Logger.Warn($ctx, $message, $*args)`,
`Logger.Info($ctx, $message, $*args)`,
`Logger.Debug($ctx, $message, $*args)`,
`$foo.Logger.Error($ctx, $message, $*args)`,
`$foo.Logger.Warn($ctx, $message, $*args)`,
`$foo.Logger.Info($ctx, $message, $*args)`,
`$foo.Logger.Debug($ctx, $message, $*args)`,
).
Where(
(
// It doesn't end with a special character:
m["message"].Text.Matches(`[.!?]"$`) ||
// it starts with lowercase:
m["message"].Text.Matches(`^"[A-Z]{1}`) &&
// but there are exceptions:
!m["message"].Text.Matches(`^"Prometheus`) &&
!m["message"].Text.Matches(`^"X11`) &&
!m["message"].Text.Matches(`^"CSP`) &&
!m["message"].Text.Matches(`^"OIDC`))).
Report(`Message $message must start with lowercase, and does not end with a special characters.`)
}
// slogMessageLength ensures that important log messages are meaningful, and must be at least 16 characters long.
func slogMessageLength(m dsl.Matcher) {
m.Import("cdr.dev/slog")
m.Match(
`logger.Error($ctx, $message, $*args)`,
`logger.Warn($ctx, $message, $*args)`,
`logger.Info($ctx, $message, $*args)`,
`$foo.logger.Error($ctx, $message, $*args)`,
`$foo.logger.Warn($ctx, $message, $*args)`,
`$foo.logger.Info($ctx, $message, $*args)`,
`Logger.Error($ctx, $message, $*args)`,
`Logger.Warn($ctx, $message, $*args)`,
`Logger.Info($ctx, $message, $*args)`,
`$foo.Logger.Error($ctx, $message, $*args)`,
`$foo.Logger.Warn($ctx, $message, $*args)`,
`$foo.Logger.Info($ctx, $message, $*args)`,
// no debug
).
Where(
// It has at least 16 characters (+ ""):
m["message"].Text.Matches(`^".{0,15}"$`) &&
// but there are exceptions:
!m["message"].Text.Matches(`^"command exit"$`)).
Report(`Message $message is too short, it must be at least 16 characters long.`)
}
// slogErr ensures that errors are logged with "slog.Error" instead of "slog.F"
func slogError(m dsl.Matcher) {
m.Import("cdr.dev/slog")
m.Match(
`slog.F($name, $value)`,
).
Where(m["name"].Const && m["value"].Type.Is("error") && !m["name"].Text.Matches(`^"internal_error"$`)).
Report(`Error should be logged using "slog.Error" instead.`)
}
// withTimezoneUTC ensures that we don't just sprinkle dbtestutil.WithTimezone("UTC") about
// to work around real timezone bugs in our code.
//
//nolint:unused,deadcode,varnamelen
func withTimezoneUTC(m dsl.Matcher) {
m.Match(
`dbtestutil.WithTimezone($tz)`,
).Where(
m["tz"].Text.Matches(`[uU][tT][cC]"$`),
).Report(`Setting database timezone to UTC may mask timezone-related bugs.`).
At(m["tz"])
}