Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee0fba8dff | |||
| da67cd3b36 | |||
| 77392cc146 | |||
| 7a2b1ac76d | |||
| e5ccf74ccc | |||
| a47ff911e1 | |||
| a8e23647c5 | |||
| 960ec18d35 | |||
| eae64160bd | |||
| b58bfebcf3 | |||
| 05124309ee | |||
| 6d1e99d6ae | |||
| 01b70dcbaa | |||
| e54ceb3b92 | |||
| f5bf6687e7 | |||
| 7cf60c4e59 | |||
| e9870049bb | |||
| cc40d6c355 | |||
| 87310838d4 | |||
| 9e7ce393c5 | |||
| 13a25ff4af | |||
| 8e051a8e2c | |||
| 67b27550cd | |||
| 3a8fa168d0 | |||
| c5a21e07a4 | |||
| 8b6a80b82d | |||
| 3a54a3132f | |||
| d004200d93 | |||
| a8d92df7d5 | |||
| 5a3ade7cd4 | |||
| b1a1103f7e | |||
| afa23b8d3c | |||
| fae52150cc | |||
| ae6cf8c366 | |||
| 04669ec2fa | |||
| 55c9829a27 | |||
| 8da68871f0 |
@@ -0,0 +1,39 @@
|
||||
## Description
|
||||
|
||||
<!-- Briefly describe what this PR does and why -->
|
||||
|
||||
---
|
||||
|
||||
## Type of Change
|
||||
|
||||
- [ ] New module
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature/enhancement
|
||||
- [ ] Documentation
|
||||
- [ ] Other
|
||||
|
||||
---
|
||||
|
||||
## Module Information
|
||||
|
||||
<!-- Delete this section if not applicable -->
|
||||
|
||||
**Path:** `registry/[namespace]/modules/[module-name]`
|
||||
**New version:** `v1.0.0`
|
||||
**Breaking change:** [ ] Yes [ ] No
|
||||
|
||||
---
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
- [ ] Tests pass (`bun test`)
|
||||
- [ ] Code formatted (`bun run fmt`)
|
||||
- [ ] Changes tested locally
|
||||
|
||||
---
|
||||
|
||||
## Related Issues
|
||||
|
||||
<!-- Link related issues or write "None" if not applicable -->
|
||||
|
||||
Closes #
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Version Bump Script
|
||||
# Usage: ./version-bump.sh <bump_type> [base_ref]
|
||||
# bump_type: patch, minor, or major
|
||||
# base_ref: base reference for diff (default: origin/main)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <bump_type> [base_ref]"
|
||||
echo " bump_type: patch, minor, or major"
|
||||
echo " base_ref: base reference for diff (default: origin/main)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 patch # Update versions with patch bump"
|
||||
echo " $0 minor # Update versions with minor bump"
|
||||
echo " $0 major # Update versions with major bump"
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_version() {
|
||||
local version="$1"
|
||||
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "❌ Invalid version format: '$version'. Expected X.Y.Z format." >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
bump_version() {
|
||||
local current_version="$1"
|
||||
local bump_type="$2"
|
||||
|
||||
IFS='.' read -r major minor patch <<< "$current_version"
|
||||
|
||||
if ! [[ "$major" =~ ^[0-9]+$ ]] || ! [[ "$minor" =~ ^[0-9]+$ ]] || ! [[ "$patch" =~ ^[0-9]+$ ]]; then
|
||||
echo "❌ Version components must be numeric: major='$major' minor='$minor' patch='$patch'" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$bump_type" in
|
||||
"patch")
|
||||
echo "$major.$minor.$((patch + 1))"
|
||||
;;
|
||||
"minor")
|
||||
echo "$major.$((minor + 1)).0"
|
||||
;;
|
||||
"major")
|
||||
echo "$((major + 1)).0.0"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_readme_version() {
|
||||
local readme_path="$1"
|
||||
local namespace="$2"
|
||||
local module_name="$3"
|
||||
local new_version="$4"
|
||||
|
||||
if [ ! -f "$readme_path" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local module_source="registry.coder.com/${namespace}/${module_name}/coder"
|
||||
if grep -q "source.*${module_source}" "$readme_path"; then
|
||||
echo "Updating version references for $namespace/$module_name in $readme_path"
|
||||
awk -v module_source="$module_source" -v new_version="$new_version" '
|
||||
/source.*=.*/ {
|
||||
if ($0 ~ module_source) {
|
||||
in_target_module = 1
|
||||
} else {
|
||||
in_target_module = 0
|
||||
}
|
||||
}
|
||||
/version.*=.*"/ {
|
||||
if (in_target_module) {
|
||||
gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"")
|
||||
in_target_module = 0
|
||||
}
|
||||
}
|
||||
{ print }
|
||||
' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path"
|
||||
return 0
|
||||
elif grep -q 'version\s*=\s*"' "$readme_path"; then
|
||||
echo "⚠️ Found version references but no module source match for $namespace/$module_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
local bump_type="$1"
|
||||
local base_ref="${2:-origin/main}"
|
||||
|
||||
case "$bump_type" in
|
||||
"patch" | "minor" | "major") ;;
|
||||
|
||||
*)
|
||||
echo "❌ Invalid bump type: '$bump_type'. Expected patch, minor, or major." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "🔍 Detecting modified modules..."
|
||||
|
||||
local changed_files
|
||||
changed_files=$(git diff --name-only "${base_ref}"...HEAD)
|
||||
local modules
|
||||
modules=$(echo "$changed_files" | grep -E '^registry/[^/]+/modules/[^/]+/' | cut -d'/' -f1-4 | sort -u)
|
||||
|
||||
if [ -z "$modules" ]; then
|
||||
echo "❌ No modules detected in changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found modules:"
|
||||
echo "$modules"
|
||||
echo ""
|
||||
|
||||
local bumped_modules=""
|
||||
local updated_readmes=""
|
||||
local untagged_modules=""
|
||||
local has_changes=false
|
||||
|
||||
while IFS= read -r module_path; do
|
||||
if [ -z "$module_path" ]; then continue; fi
|
||||
|
||||
local namespace
|
||||
namespace=$(echo "$module_path" | cut -d'/' -f2)
|
||||
local module_name
|
||||
module_name=$(echo "$module_path" | cut -d'/' -f4)
|
||||
|
||||
echo "📦 Processing: $namespace/$module_name"
|
||||
|
||||
local latest_tag
|
||||
latest_tag=$(git tag -l "release/${namespace}/${module_name}/v*" | sort -V | tail -1)
|
||||
local readme_path="$module_path/README.md"
|
||||
local current_version
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then
|
||||
local readme_version
|
||||
readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/')
|
||||
echo "No git tag found, but README shows version: $readme_version"
|
||||
|
||||
if ! validate_version "$readme_version"; then
|
||||
echo "Starting from v1.0.0 instead"
|
||||
current_version="1.0.0"
|
||||
else
|
||||
current_version="$readme_version"
|
||||
untagged_modules="$untagged_modules\n- $namespace/$module_name (README: v$readme_version)"
|
||||
fi
|
||||
else
|
||||
echo "No existing tags or version references found for $namespace/$module_name, starting from v1.0.0"
|
||||
current_version="1.0.0"
|
||||
fi
|
||||
else
|
||||
current_version=$(echo "$latest_tag" | sed 's/.*\/v//')
|
||||
echo "Found git tag: $latest_tag (v$current_version)"
|
||||
fi
|
||||
|
||||
echo "Current version: $current_version"
|
||||
|
||||
if ! validate_version "$current_version"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local new_version
|
||||
new_version=$(bump_version "$current_version" "$bump_type")
|
||||
|
||||
echo "New version: $new_version"
|
||||
|
||||
if update_readme_version "$readme_path" "$namespace" "$module_name" "$new_version"; then
|
||||
updated_readmes="$updated_readmes\n- $namespace/$module_name"
|
||||
has_changes=true
|
||||
fi
|
||||
|
||||
bumped_modules="$bumped_modules\n- $namespace/$module_name: v$current_version → v$new_version"
|
||||
echo ""
|
||||
|
||||
done <<< "$modules"
|
||||
|
||||
# Always run formatter to ensure consistent formatting
|
||||
echo "🔧 Running formatter to ensure consistent formatting..."
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
bun fmt >/dev/null 2>&1 || echo "⚠️ Warning: bun fmt failed, but continuing..."
|
||||
else
|
||||
echo "⚠️ Warning: bun not found, skipping formatting"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "📋 Summary:"
|
||||
echo "Bump Type: $bump_type"
|
||||
echo ""
|
||||
echo "Modules Updated:"
|
||||
echo -e "$bumped_modules"
|
||||
echo ""
|
||||
|
||||
if [ -n "$updated_readmes" ]; then
|
||||
echo "READMEs Updated:"
|
||||
echo -e "$updated_readmes"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ -n "$untagged_modules" ]; then
|
||||
echo "⚠️ Modules Without Git Tags:"
|
||||
echo -e "$untagged_modules"
|
||||
echo "These modules were versioned based on README content. Consider creating proper release tags after merging."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$has_changes" = true ]; then
|
||||
echo "✅ Version bump completed successfully!"
|
||||
echo "📝 README files have been updated with new versions."
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Review the changes: git diff"
|
||||
echo "2. Commit the changes: git add . && git commit -m 'chore: bump module versions ($bump_type)'"
|
||||
echo "3. Push the changes: git push"
|
||||
exit 0
|
||||
else
|
||||
echo "ℹ️ No README files were updated (no version references found matching module sources)."
|
||||
echo "Version calculations completed, but no files were modified."
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,4 +1,7 @@
|
||||
[default.extend-words]
|
||||
muc = "muc" # For Munich location code
|
||||
Hashi = "Hashi"
|
||||
HashiCorp = "HashiCorp"
|
||||
HashiCorp = "HashiCorp"
|
||||
|
||||
[files]
|
||||
extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.32.0
|
||||
uses: crate-ci/typos@v1.33.1
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
@@ -2,8 +2,6 @@ name: deploy-registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
# Matches release/<namespace>/<resource_name>/<semantic_version>
|
||||
# (e.g., "release/whizus/exoscale-zone/v1.0.13")
|
||||
@@ -22,15 +20,14 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Authenticate with Google Cloud
|
||||
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935
|
||||
uses: google-github-actions/auth@ba79af03959ebeac9769e648f473a284504d9193
|
||||
with:
|
||||
workload_identity_provider: projects/309789351055/locations/global/workloadIdentityPools/github-actions/providers/github
|
||||
service_account: registry-v2-github@coder-registry-1.iam.gserviceaccount.com
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a
|
||||
# For the time being, let's have the first couple merges to main in
|
||||
# modules deploy a new version to *dev*. Once we review and make sure
|
||||
# everything's working, we can deploy a new version to *main*. Maybe in
|
||||
# the future we could automate this based on the result of E2E tests.
|
||||
- name: Deploy to dev.registry.coder.com
|
||||
run: gcloud builds triggers run 29818181-126d-4f8a-a937-f228b27d3d34 --branch dev
|
||||
- name: Deploy to registry.coder.com
|
||||
run: |
|
||||
gcloud builds triggers run 106610ff-41fb-4bd0-90a2-7643583fb9c0 --branch main
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
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
|
||||
@@ -0,0 +1,120 @@
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
paths:
|
||||
- "registry/**/modules/**"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
version-bump:
|
||||
if: github.event.label.name == 'version:patch' || github.event.label.name == 'version:minor' || github.event.label.name == 'version:major'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Set up Terraform
|
||||
uses: coder/coder/.github/actions/setup-tf@main
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
- name: Extract bump type from label
|
||||
id: bump-type
|
||||
run: |
|
||||
case "${{ github.event.label.name }}" in
|
||||
"version:patch")
|
||||
echo "type=patch" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"version:minor")
|
||||
echo "type=minor" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"version:major")
|
||||
echo "type=major" >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo "Invalid version label: ${{ github.event.label.name }}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Check version bump requirements
|
||||
id: version-check
|
||||
run: |
|
||||
output_file=$(mktemp)
|
||||
if ./.github/scripts/version-bump.sh "${{ steps.bump-type.outputs.type }}" origin/main > "$output_file" 2>&1; then
|
||||
echo "Script completed successfully"
|
||||
else
|
||||
echo "Script failed"
|
||||
cat "$output_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "output<<EOF"
|
||||
cat "$output_file"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
cat "$output_file"
|
||||
|
||||
if git diff --quiet; then
|
||||
echo "versions_up_to_date=true" >> $GITHUB_OUTPUT
|
||||
echo "✅ All module versions are already up to date"
|
||||
else
|
||||
echo "versions_up_to_date=false" >> $GITHUB_OUTPUT
|
||||
echo "❌ Module versions need to be updated"
|
||||
echo "Files that would be changed:"
|
||||
git diff --name-only
|
||||
echo ""
|
||||
echo "Diff preview:"
|
||||
git diff
|
||||
|
||||
git checkout .
|
||||
git clean -fd
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.version-check.outputs.versions_up_to_date == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const output = `${{ steps.version-check.outputs.output }}`;
|
||||
const bumpType = `${{ steps.bump-type.outputs.type }}`;
|
||||
|
||||
let comment = `## ❌ Version Bump Validation Failed\n\n`;
|
||||
comment += `**Bump Type:** \`${bumpType}\`\n\n`;
|
||||
comment += `Module versions need to be updated but haven't been bumped yet.\n\n`;
|
||||
comment += `**Required Actions:**\n`;
|
||||
comment += `1. Run the version bump script locally: \`./.github/scripts/version-bump.sh ${bumpType}\`\n`;
|
||||
comment += `2. Commit the changes: \`git add . && git commit -m "chore: bump module versions (${bumpType})"\`\n`;
|
||||
comment += `3. Push the changes: \`git push\`\n\n`;
|
||||
comment += `### Script Output:\n\`\`\`\n${output}\n\`\`\`\n\n`;
|
||||
comment += `> Please update the module versions and push the changes to continue.`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
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$
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
@@ -1,268 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="katman_1" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 841.9 595.3">
|
||||
<!-- Generator: Adobe Illustrator 29.3.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 151) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0 {
|
||||
fill: #2e3c4e;
|
||||
}
|
||||
|
||||
.st1 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.st9 {
|
||||
fill: #7300e5;
|
||||
}
|
||||
|
||||
.st10, .st11 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.st12 {
|
||||
fill: url(#Adsız_degrade_4);
|
||||
}
|
||||
|
||||
.st2 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st13 {
|
||||
fill: url(#Adsız_degrade_41);
|
||||
}
|
||||
|
||||
.st14 {
|
||||
fill: #d7c8f9;
|
||||
}
|
||||
|
||||
.st15 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.st16 {
|
||||
stroke: #d1d1d6;
|
||||
stroke-width: .5px;
|
||||
}
|
||||
|
||||
.st16, .st17, .st11 {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.st3 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st17 {
|
||||
stroke: #fff;
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.st4 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st5 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st7 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st18 {
|
||||
fill: url(#Adsız_degrade_5);
|
||||
}
|
||||
|
||||
.st8 {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.st19 {
|
||||
fill: url(#Adsız_degrade_2);
|
||||
}
|
||||
</style>
|
||||
<mask id="mask" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1058_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st14" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_2" data-name="Adsız degrade 2" x1="68.8" y1="563" x2="-1.8" y2="563.4" gradientTransform="translate(194.4 765.6) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".7"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".4"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-1" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1038_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_4" data-name="Adsız degrade 4" x1="69.7" y1="567.5" x2="-11.4" y2="566.9" gradientTransform="translate(178.6 1012.1) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#fff"/>
|
||||
<stop offset=".5" stop-color="#fff" stop-opacity=".6"/>
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".2"/>
|
||||
</linearGradient>
|
||||
<mask id="mask-2" x="69.2" y="33.9" width="704" height="528" maskUnits="userSpaceOnUse">
|
||||
<g id="lottie-ymehjmywpqh__lottie_element_1018_2">
|
||||
<g>
|
||||
<rect class="st11" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st3">
|
||||
<path class="st9" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="Adsız_degrade_41" data-name="Adsız degrade 4" x1="21.7" y1="568.3" x2="163.7" y2="568.3" gradientTransform="translate(551.4 969.2) scale(1 -1)" xlink:href="#Adsız_degrade_4"/>
|
||||
<linearGradient id="Adsız_degrade_5" data-name="Adsız degrade 5" x1="56.8" y1="647.8" x2="-1.1" y2="536.3" gradientTransform="translate(349.3 2027.2) scale(3 -3)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2fabff"/>
|
||||
<stop offset=".3" stop-color="#5570ff"/>
|
||||
<stop offset=".6" stop-color="#7b36ff"/>
|
||||
<stop offset=".8" stop-color="#6a2cdc"/>
|
||||
<stop offset="1" stop-color="#5921b8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect class="st15" x="69.2" y="33.9" width="704" height="528"/>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st2">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st1">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st5">
|
||||
<path class="st17" d="M424.4,138.3c-2.9-1.6-7.6-1.6-10.5,0l-130.4,69.8c-2.9,1.6-5.3,5.5-5.3,8.8v154.3c0,3.3,2.4,7.3,5.2,8.9l130.4,72.3c2.9,1.6,7.6,1.6,10.5,0l131.5-72.1c2.9-1.6,5.3-5.6,5.3-8.9v-151.8c0-3.3-2.4-7.3-5.3-8.9l-131.5-72.5Z"/>
|
||||
</g>
|
||||
<g class="st4">
|
||||
<path class="st17" d="M424.4,139.4c-2.9-1.6-7.6-1.6-10.5,0l-129.6,69.3c-2.9,1.6-5.3,5.5-5.3,8.8v153.3c0,3.3,2.4,7.3,5.2,8.9l129.6,71.9c2.9,1.6,7.6,1.6,10.5,0l130.7-71.6c2.9-1.6,5.3-5.6,5.3-8.9v-150.9c0-3.3-2.4-7.3-5.3-8.9l-130.7-72Z"/>
|
||||
</g>
|
||||
<g class="st8">
|
||||
<path class="st17" d="M424.4,157.4c-2.9-1.6-7.7-1.6-10.6,0l-115.9,61.1c-2.9,1.5-5.3,5.5-5.3,8.8v135.9c0,3.3,2.4,7.3,5.2,8.9l116,64.5c2.9,1.6,7.6,1.6,10.5,0l117.1-63.3c2.9-1.6,5.3-5.5,5.3-8.9v-135c0-3.3-2.4-7.3-5.3-8.8l-117.1-63.2Z"/>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st19" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M324.5,185.8v33.5c0,4.4-3.6,8-8,8h-107.8c-4.4,0-8-3.6-8-8v-33.5c0-4.4,3.6-8,8-8h107.8c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M236.1,209.6c.2,0,.3-.1.4-.2,0,0,0-.3,0-.5v-1.3c0-.3-.1-.5-.3-.5s-.2,0-.4,0c-.4,0-.8.2-1.3.2-.5,0-.9,0-1.2,0-1.4,0-2.4-.4-3.1-1.1s-1-1.8-1-3.3v-.5c0-1.5.3-2.6,1-3.4s1.7-1.1,3.1-1.1,1.4,0,2.2.3c.2,0,.4,0,.4,0,.2,0,.3-.1.3-.5v-1.3c0-.2,0-.4,0-.5s-.2-.2-.3-.2c-1-.3-1.9-.4-2.9-.4-2.2,0-3.9.6-5.1,1.9-1.2,1.3-1.8,3.1-1.8,5.4s.6,4.1,1.7,5.3c1.2,1.2,2.9,1.8,5.1,1.8s2.2-.1,3.2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M240.4,204.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM246.2,208.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M252.3,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M267.7,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M272.5,203.6c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM278.3,209.6c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M283.6,209.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M299.1,209.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st12" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
<path class="st16" d="M329.4,427.5v34c0,4.4-3.6,8-8,8h-127.2c-4.4.1-8-3.5-8-7.9v-34c0-4.4,3.6-8,8-8h127.2c4.4-.1,8,3.5,8,7.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M213.9,439.7h1.7c2.4,0,3.6,1.5,3.6,4.4v.4c0,2.9-1.2,4.4-3.6,4.4h-1.7v-9.2ZM215.9,451.2c2,0,3.6-.6,4.7-1.8,1.1-1.2,1.7-2.9,1.7-5.1s-.6-3.9-1.7-5.1c-1.1-1.2-2.7-1.8-4.8-1.8h-4.5c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v12.9c0,.2,0,.3.1.4,0,0,.2.1.4.1h4.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M227.4,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM229.1,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M241.5,451.1c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M246.6,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM248.3,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M263,450.8c.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1,.1-1.5.3c-.5.2-1,.5-1.4.8v-4.9c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.5c.3,0,.5-.2.6-.4v-.6c.5.4.9.7,1.5.9.5.2,1.1.3,1.7.3s1.6-.2,2.2-.7ZM257.9,448.7v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M269.8,449.3c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM271.5,451.2c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M284.1,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M289.3,445c0-.8.3-1.3.7-1.7.4-.4.9-.6,1.6-.6,1.1,0,1.7.7,1.7,2v.3h-3.9ZM295.1,451c.2,0,.3-.1.4-.2,0,0,0-.2,0-.4v-.9c0-.3-.1-.5-.3-.5s0,0-.1,0h-.1c-1,.3-1.8.4-2.6.4s-1.8-.2-2.3-.6-.8-1-.8-1.9h5.8c.2,0,.3,0,.4,0s.1-.2.2-.4c0-.5,0-1,0-1.4,0-1.3-.4-2.4-1.1-3.1-.7-.7-1.7-1.1-3-1.1s-2.8.5-3.7,1.4c-.9,1-1.3,2.3-1.3,4s.4,3.1,1.3,4c.9.9,2.2,1.4,3.9,1.4s2.2-.2,3.2-.6Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M303.8,450.7c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<g>
|
||||
<path class="st13" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
<path class="st16" d="M689.3,383.8v34.2c-.1,4.4-3.7,8-8.1,8h-150.3c-4.4,0-8-3.6-8-8v-34.2c.1-4.4,3.7-8,8.1-8h150.2c4.4,0,8,3.6,8,8Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<g class="st6">
|
||||
<path class="st0" d="M549.5,407.8c.2,0,.4,0,.5-.1,0,0,.2-.2.2-.4l.8-2.6h4.8l.8,2.6c0,.2.2.3.2.4.1,0,.2.1.4.1h2.2c.3,0,.4-.1.4-.3s0-.1,0-.2c0,0,0-.2-.1-.3l-4.4-12.5c0-.2-.1-.3-.2-.4,0,0-.3-.1-.5-.1h-2.2c-.2,0-.3,0-.3,0,0,0-.1,0-.2.2,0,0-.1.2-.2.3l-4.4,12.5c0,.1,0,.2-.1.3,0,.1,0,.2,0,.2,0,.2.1.3.4.3h2.1ZM551.7,402.5l1.7-5.8,1.8,5.8h-3.5Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M564.9,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM564.4,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M577.2,405.3v-5.3c.6-.4,1.3-.5,2-.5s1.3.2,1.7.8.5,1.3.5,2.4-.2,1.9-.5,2.4-.9.8-1.6.8-1.4-.2-2-.5ZM576.7,412c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-4.4c.3.3.8.5,1.3.7.5.2,1,.3,1.6.3.8,0,1.6-.2,2.2-.7.6-.4,1.1-1.1,1.5-1.9.4-.8.6-1.8.6-2.9,0-1.6-.4-2.9-1.1-3.8-.8-.9-1.8-1.4-3.1-1.4s-1.2.1-1.7.3c-.6.2-1,.5-1.4.9v-.5c-.2-.3-.4-.4-.7-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v13.3c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M590.6,407.8c.2,0,.3-.1.4-.2,0,0,0-.2,0-.5v-1c0-.2,0-.3,0-.3,0,0-.1-.1-.3-.1s-.1,0-.2,0c0,0-.2,0-.3,0-.5,0-.7-.3-.7-1v-11.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v11.4c0,2,.9,3,2.7,3s1,0,1.4-.2Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M595.8,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM595.5,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M604.6,408c.4,0,.8-.2,1.2-.3.2,0,.3-.1.3-.2,0,0,0-.2,0-.4v-1c0-.3-.1-.4-.3-.4s-.2,0-.3,0c-.6.2-1.1.2-1.6.2-.9,0-1.6-.2-2-.7-.4-.5-.6-1.2-.6-2.2v-.3c0-1,.2-1.8.7-2.2.4-.5,1.1-.7,2.1-.7s1,0,1.6.2c0,0,0,0,.1,0,0,0,0,0,.1,0,.2,0,.3-.2.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.4-.2-.7-.3-1.5-.4-2.3-.4-1.6,0-2.9.5-3.8,1.4-.9,1-1.4,2.3-1.4,4s.4,3,1.3,3.9c.9.9,2.1,1.4,3.7,1.4s.8,0,1.2-.1Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M610.7,405.9c-.2-.2-.3-.5-.3-1,0-.9.6-1.4,1.7-1.4s1,0,1.6.1v1.7c-.3.2-.6.5-1,.6s-.7.2-1,.2-.7-.1-1-.3ZM612.4,407.8c.5-.2,1-.5,1.4-.9v.5c.2.2.2.3.3.3,0,0,.2.1.4.1h1.4c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-6.6c0-1.2-.3-2.1-1-2.6-.6-.6-1.6-.8-3-.8s-1.3,0-2,.2c-.7.1-1.2.3-1.7.5-.2,0-.3.2-.4.2,0,0,0,.2,0,.4v.9c0,.3.1.5.3.5s0,0,.1,0c0,0,0,0,.1,0,1.1-.3,2.2-.5,3.1-.5s1.2.1,1.4.3c.2.2.4.7.4,1.3v1c-.8-.2-1.5-.3-2.1-.3s-1.5.1-2.1.4c-.6.3-1.1.6-1.4,1.1-.3.5-.5,1.1-.5,1.7,0,.9.3,1.7.9,2.2.6.6,1.4.8,2.4.8s1-.1,1.6-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M624.7,407.7c.2,0,.3-.1.3-.2,0,0,0-.2,0-.5v-1c0-.3-.1-.4-.3-.4s-.3,0-.5,0c-.2,0-.4,0-.6,0-.5,0-.9,0-1.1-.3-.2-.2-.3-.5-.3-.9v-4.8h2.3c.2,0,.3,0,.4-.1s.1-.2.1-.4v-1.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-2.3v-2.3c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.2.4l-.4,2.3-1.1.2c-.2,0-.3,0-.4.2,0,0-.1.2-.1.4v.8c0,.2,0,.3.1.4,0,0,.2.1.4.1h1v4.9c0,1.1.3,1.9.8,2.5.5.5,1.4.8,2.5.8s1.4,0,2-.3Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M630.1,395.6c.3-.3.5-.7.5-1.1s-.2-.9-.5-1.1-.7-.4-1.2-.4-.9.1-1.2.4c-.3.3-.5.7-.5,1.1s.1.9.5,1.1c.3.3.7.4,1.2.4s.9-.1,1.2-.4ZM629.9,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-9.2c0-.2,0-.3-.1-.4,0,0-.2-.1-.4-.1h-1.9c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M635.7,402.7c0-2.1.7-3.2,2.1-3.2s2.1,1,2.1,3.2-.7,3.2-2.1,3.2-2.1-1.1-2.1-3.2ZM641.5,406.7c.9-.9,1.3-2.3,1.3-4s-.4-3-1.3-4c-.9-.9-2.1-1.4-3.7-1.4s-2.8.5-3.7,1.4c-.9.9-1.3,2.3-1.3,4s.5,3,1.3,4c.9.9,2.1,1.4,3.7,1.4s2.8-.5,3.7-1.4Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M647.6,407.8c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c.7-.5,1.4-.7,2.2-.7s.9.1,1.1.4c.2.2.4.7.4,1.2v6.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9c.2,0,.3,0,.4-.1,0,0,.1-.2.1-.4v-7.1c0-1-.3-1.7-.8-2.2-.5-.5-1.2-.8-2.2-.8-1.4,0-2.6.5-3.8,1.4l-.2-.7c0-.3-.2-.4-.6-.4h-1.4c-.2,0-.3,0-.4.1,0,0-.1.2-.1.4v9.2c0,.2,0,.3.1.4,0,0,.2.1.4.1h1.9Z"/>
|
||||
</g>
|
||||
<g class="st6">
|
||||
<path class="st0" d="M663.3,407.3c.8-.6,1.1-1.4,1.1-2.4s-.2-1.3-.5-1.7c-.3-.4-.9-.8-1.8-1.2l-1.5-.6c-.5-.2-.8-.4-1-.5-.2-.1-.2-.3-.2-.6s.1-.5.4-.6c.3-.1.6-.2,1.2-.2s1.4,0,1.9.2c.3,0,.5.1.6.1.2,0,.3-.1.3-.5v-.9c0-.2,0-.4,0-.5,0,0-.2-.2-.3-.2-.8-.3-1.7-.5-2.7-.5s-2.2.3-2.9.8c-.7.5-1.1,1.3-1.1,2.2s.2,1.3.5,1.8c.4.5,1,.9,1.8,1.2l1.6.6c.5.2.8.3.9.5.1.2.2.4.2.6,0,.7-.5,1-1.4,1s-1,0-1.5-.1c-.5,0-1-.2-1.5-.3-.1,0-.2,0-.3,0-.2,0-.3.1-.3.5v.9c0,.2,0,.3,0,.4,0,0,.2.2.4.2.9.4,2,.6,3.2.6s2.2-.3,3-.9Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st18" d="M452.1,74c-19.6-11.3-43.8-11.3-63.4,0l-145.3,83.9c-19.6,11.3-31.7,32.3-31.7,54.9v167.8c0,22.7,12.1,43.6,31.7,54.9l145.3,83.9c19.6,11.3,43.8,11.3,63.4,0l145.3-83.9c19.6-11.3,31.7-32.3,31.7-54.9v-167.8c0-22.7-12.1-43.6-31.7-54.9l-145.3-83.9Z"/>
|
||||
<path class="st10" d="M438.6,293.4l-12.7,25.4,120.5,69.8,12.7-25.4-120.5-69.8ZM422.7,269.8c-2-1.1-4.4-1.1-6.3,0l-21.1,12.2c-2,1.1-3.2,3.2-3.2,5.5v24.4c0,2.3,1.2,4.4,3.2,5.5l21.1,12.2c2,1.1,4.4,1.1,6.3,0l21.1-12.2c2-1.1,3.2-3.2,3.2-5.5v-24.4c0-2.3-1.2-4.4-3.2-5.5l-21.1-12.2ZM411.6,163.4c7.9-4.5,17.5-4.5,25.4,0l98.2,56.7c7.9,4.5,12.7,12.9,12.7,22v113.4c0,9.1-4.8,17.4-12.7,22l-98.2,56.7c-7.9,4.5-17.5,4.5-25.4,0l-98.2-56.7c-7.9-4.5-12.7-12.9-12.7-22v-113.4c0-9.1,4.8-17.4,12.7-22l98.2-56.7ZM395.7,135.2l-103.1,59.5c-15.7,9.1-25.4,25.8-25.4,44v119c0,18.1,9.7,34.9,25.4,44l103.1,59.5c15.7,9.1,35,9.1,50.8,0l103.1-59.5c15.7-9.1,25.4-25.8,25.4-44v-119c0-18.1-9.7-34.9-25.4-44l-103.1-59.5c-15.7-9.1-35-9.1-50.8,0Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg viewBox="0 0 106.14 115.53">
|
||||
<path
|
||||
d="M60.78,7.84c-4.36-2.52-9.73-2.52-14.08,0L14.44,26.47c-4.36,2.52-7.04,7.17-7.04,12.2v37.26c0,5.03,2.68,9.68,7.04,12.2l32.26,18.63c4.36,2.52,9.73,2.52,14.08,0l32.26-18.63c4.36-2.52,7.04-7.17,7.04-12.2v-37.26c0-5.03-2.68-9.68-7.04-12.2L60.78,7.84Z"
|
||||
style="fill:url(#gradient)" />
|
||||
<linearGradient id="gradient" x1="-228.54" y1="480.48" x2="-246.11" y2="446.65"
|
||||
gradientTransform="translate(569.95 1056) scale(2.16 -2.16)"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2fabff" />
|
||||
<stop offset=".31" stop-color="#5570ff" />
|
||||
<stop offset=".62" stop-color="#7b36ff" />
|
||||
<stop offset=".81" stop-color="#6a2cdc" />
|
||||
<stop offset="1" stop-color="#5921b8" />
|
||||
</linearGradient>
|
||||
<path
|
||||
d="M48.26,21.44l-22.88,13.21c-3.49,2.01-5.63,5.73-5.63,9.76v26.43c0,4.03,2.15,7.74,5.63,9.76l22.88,13.21c3.49,2.01,7.78,2.01,11.27,0l22.88-13.21c3.49-2.01,5.63-5.73,5.63-9.76v-26.43c0-4.03-2.15-7.74-5.63-9.76l-22.88-13.21c-3.49-2.01-7.78-2.01-11.27,0ZM51.78,27.7c1.74-1.01,3.89-1.01,5.63,0l21.81,12.59c1.74,1.01,2.82,2.86,2.82,4.88v25.19c0,2.01-1.07,3.87-2.82,4.88l-21.81,12.59c-1.74,1.01-3.89,1.01-5.63,0l-21.81-12.59c-1.74-1.01-2.82-2.86-2.82-4.88v-25.19c0-2.01,1.07-3.87,2.82-4.88l21.81-12.59ZM54.25,51.32c-.44-.25-.97-.25-1.41,0l-4.69,2.71c-.44.25-.7.72-.7,1.22v5.42c0,.5.27.97.7,1.22l4.69,2.71c.44.25.97.25,1.41,0l4.69-2.71c.44-.25.7-.72.7-1.22v-5.42c0-.5-.27-.97-.7-1.22l-4.69-2.71ZM57.77,56.55l-2.82,5.63,26.76,15.49,2.82-5.63-26.76-15.49Z"
|
||||
style="fill:#fff; stroke:#fff; stroke-width:2.93px;" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg version="1.1" baseProfile="full" width="340" height="310" xmlns="http://www.w3.org/2000/svg">
|
||||
<g stroke="white" stroke-width="1.5">
|
||||
<g transform="rotate(30) skewX(30)">
|
||||
<rect x="110" y="-72" width="175" height="75" fill="#333333" transform="skewX(-50)"/>
|
||||
<rect x="110" y="3" width="87.5" height="75" fill="#CDCDCD" transform="skewX(-50)"/>
|
||||
<rect x="16.5" y="78.9" width="87.5" height="25" fill="#CDCDCD" />
|
||||
<rect x="16.5" y="104.5" width="175" height="25" fill="#888888" />
|
||||
<rect x="16.5" y="130" width="175" height="50" fill="#DD4814" />
|
||||
<rect x="104" y="166" width="89.5" height="25" fill="#CDCDCD" transform="skewY(-40)"/>
|
||||
<rect x="228.3" y="29.5" width="87.5" height="75" fill="#888888" transform="skewX(-50)"/>
|
||||
<rect x="191.8" y="266" width="89.5" height="25" fill="#888888" transform="skewY(-40)"/>
|
||||
<rect x="192" y="291" width="179.5" height="50" fill="#DD4814" transform="skewY(-40)"/>
|
||||
<rect x="282.3" y="240" width="89.1" height="50" fill="#333333" transform="skewY(-40)"/>
|
||||
<rect x="194" y="3.7" width="87.5" height="25" fill="#333333" />
|
||||
</g>
|
||||
<line x1="93" y1="57" x2="93" y2="88" />
|
||||
<line x1="169" y1="131" x2="92" y2="88" />
|
||||
<line x1="92" y1="88" x2="14" y2="128" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="#06D092" d="M8 0L1 4v8l7 4 7-4V4L8 0zm3.119 8.797L9.254 9.863 7.001 8.65v2.549l-2.118 1.33v-5.33l1.68-1.018 2.332 1.216V4.794l2.23-1.322-.006 5.325z"/></svg>
|
||||
|
After Width: | Height: | Size: 390 B |
@@ -1,294 +1,319 @@
|
||||
# Contributing
|
||||
# Contributing to the Coder Registry
|
||||
|
||||
## Getting started
|
||||
Welcome! This guide covers how to contribute to the Coder Registry, whether you're creating a new module or improving an existing one.
|
||||
|
||||
This repo uses two main runtimes to verify the correctness of a module/template before it is published:
|
||||
## What is the Coder Registry?
|
||||
|
||||
- [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).
|
||||
The Coder Registry is a collection of Terraform modules that extend Coder workspaces with development tools like VS Code, Cursor, JetBrains IDEs, and more.
|
||||
|
||||
### Installing Bun
|
||||
## Types of Contributions
|
||||
|
||||
To install Bun, you can run this command on Linux/MacOS:
|
||||
- **[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
|
||||
|
||||
```shell
|
||||
## 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
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
```
|
||||
|
||||
Or this command on Windows:
|
||||
Install project dependencies:
|
||||
|
||||
```shell
|
||||
powershell -c "irm bun.sh/install.ps1 | iex"
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
Follow the instructions to ensure that Bun is available globally. Once Bun is installed, install all necessary dependencies from the root of the repo:
|
||||
### Understanding Namespaces
|
||||
|
||||
Via NPM:
|
||||
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.
|
||||
|
||||
```shell
|
||||
npm i
|
||||
### 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
|
||||
```
|
||||
|
||||
Via PNPM:
|
||||
#### Add Your Avatar
|
||||
|
||||
```shell
|
||||
pnpm i
|
||||
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.
|
||||
```
|
||||
|
||||
This repo does not support Yarn.
|
||||
> **Note**: The `avatar_url` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
|
||||
|
||||
### Installing Go (optional)
|
||||
### 2. Generate Module Files
|
||||
|
||||
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
|
||||
```bash
|
||||
./scripts/new_module.sh [your-username]/[module-name]
|
||||
cd registry/[your-username]/modules/[module-name]
|
||||
```
|
||||
|
||||
## Namespaces
|
||||
This script generates:
|
||||
|
||||
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.
|
||||
- `main.tf` - Terraform configuration template
|
||||
- `README.md` - Documentation template with frontmatter
|
||||
- `run.sh` - Script for module execution (can be deleted if not required)
|
||||
|
||||
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.)
|
||||
### 3. Build Your Module
|
||||
|
||||
### Namespace (contributor profile) README files
|
||||
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
|
||||
|
||||
More information about contributor profile README files can be found below.
|
||||
### 4. Test and Submit
|
||||
|
||||
### Images
|
||||
```bash
|
||||
# Test your module
|
||||
bun test -t 'module-name'
|
||||
|
||||
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.
|
||||
# Format code
|
||||
bun fmt
|
||||
|
||||
## 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
|
||||
# Commit and create PR
|
||||
git add .
|
||||
git commit -m "Add [module-name] module"
|
||||
git push origin your-branch
|
||||
```
|
||||
|
||||
You can also create a module file manually by creating the necessary files and directories.
|
||||
> **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.
|
||||
|
||||
### The composition of a Coder module
|
||||
---
|
||||
|
||||
Each Coder Module must contain the following files:
|
||||
## Contributing to Existing Modules
|
||||
|
||||
- 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)
|
||||
### 1. Find the Module
|
||||
|
||||
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";
|
||||
```bash
|
||||
find registry -name "*[module-name]*" -type d
|
||||
```
|
||||
|
||||
> [!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.
|
||||
### 2. Make Your Changes
|
||||
|
||||
#### Running tests
|
||||
**For bug fixes:**
|
||||
|
||||
You can run all tests by running this command from the root of the Registry directory:
|
||||
- Reproduce the issue
|
||||
- Fix the code in `main.tf`
|
||||
- Add/update tests
|
||||
- Update documentation if needed
|
||||
|
||||
```shell
|
||||
**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
|
||||
bun test
|
||||
```
|
||||
|
||||
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.
|
||||
### 4. Maintain Backward Compatibility
|
||||
|
||||
To run specific tests, you can use the `-t` flag, which accepts a filepath regex:
|
||||
- New variables should have default values
|
||||
- Don't break existing functionality
|
||||
- Test that minimal configurations still work
|
||||
|
||||
```shell
|
||||
bun test -t '<regex_pattern>'
|
||||
---
|
||||
|
||||
## 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
|
||||
---
|
||||
```
|
||||
|
||||
To ensure that the module runs predictably in local development, you can update the Terraform source as follows:
|
||||
### README Requirements
|
||||
|
||||
```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>"
|
||||
}
|
||||
All README files must follow these rules:
|
||||
|
||||
- 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
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use descriptive variable names and descriptions
|
||||
- Include helpful comments
|
||||
- Test all functionality
|
||||
- Follow existing code patterns in the module
|
||||
|
||||
---
|
||||
|
||||
## Versioning Guidelines
|
||||
|
||||
When you modify a module, you need to update its version number in the README. Understanding version numbers helps you describe the impact of your changes:
|
||||
|
||||
- **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)
|
||||
|
||||
### Updating Module Versions
|
||||
|
||||
If your changes require a version bump, use the version bump script:
|
||||
|
||||
```bash
|
||||
# For bug fixes
|
||||
./.github/scripts/version-bump.sh patch
|
||||
|
||||
# For new features
|
||||
./.github/scripts/version-bump.sh minor
|
||||
|
||||
# For breaking changes
|
||||
./.github/scripts/version-bump.sh major
|
||||
```
|
||||
|
||||
## Updating README files
|
||||
The script will:
|
||||
|
||||
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.
|
||||
1. Detect which modules you've modified
|
||||
2. Calculate the new version number
|
||||
3. Update all version references in the module's README
|
||||
4. Show you a summary of changes
|
||||
|
||||
### Validating all README files
|
||||
**Important**: Only run the version bump script if your changes require a new release. Documentation-only changes don't need version updates.
|
||||
|
||||
To validate all README files throughout the entire repo, you can run the following:
|
||||
---
|
||||
|
||||
```shell
|
||||
go build ./cmd/readmevalidation && ./readmevalidation
|
||||
```
|
||||
## Reporting Issues
|
||||
|
||||
The resulting binary is already part of the `.gitignore` file, but you can remove it with:
|
||||
When reporting bugs, include:
|
||||
|
||||
```shell
|
||||
rm ./readmevalidation
|
||||
```
|
||||
- Module name and version
|
||||
- Expected vs actual behavior
|
||||
- Minimal reproduction case
|
||||
- Error messages
|
||||
- Environment details (OS, Terraform version)
|
||||
|
||||
### README validation criteria
|
||||
---
|
||||
|
||||
The following criteria exists for two reasons:
|
||||
## Getting Help
|
||||
|
||||
1. Content accessibility
|
||||
2. Having content be designed in a way that's easy for the Registry site build step to use
|
||||
- **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
|
||||
|
||||
#### General README requirements
|
||||
## Common Pitfalls
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
```txt
|
||||
\`\`\`tf
|
||||
Content
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
#### Namespace (contributor profile) criteria
|
||||
|
||||
In addition to the general criteria, all README files must have the following:
|
||||
|
||||
- Frontmatter metadata with support for the following fields:
|
||||
|
||||
- `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.
|
||||
|
||||
- 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.
|
||||
Happy contributing! 🚀
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# 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.
|
||||
@@ -2,9 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -12,10 +11,18 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var supportedResourceTypes = []string{"modules", "templates"}
|
||||
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+=`)
|
||||
)
|
||||
|
||||
type coderResourceFrontmatter struct {
|
||||
Description string `yaml:"description"`
|
||||
@@ -27,7 +34,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
|
||||
@@ -37,61 +44,60 @@ type coderResourceReadme struct {
|
||||
|
||||
func validateCoderResourceDisplayName(displayName *string) error {
|
||||
if displayName != nil && *displayName == "" {
|
||||
return errors.New("if defined, display_name must not be empty string")
|
||||
return xerrors.New("if defined, display_name must not be empty string")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceDescription(description string) error {
|
||||
if description == "" {
|
||||
return errors.New("frontmatter description cannot be empty")
|
||||
return xerrors.New("frontmatter description cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCoderResourceIconURL(iconURL string) []error {
|
||||
problems := []error{}
|
||||
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 {
|
||||
if iconURL == "" {
|
||||
problems = append(problems, errors.New("icon URL cannot be empty"))
|
||||
return problems
|
||||
return []error{xerrors.New("icon URL cannot be empty")}
|
||||
}
|
||||
|
||||
isAbsoluteURL := !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/")
|
||||
if isAbsoluteURL {
|
||||
errs := []error{}
|
||||
|
||||
// If the URL does not have a relative path.
|
||||
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
|
||||
if _, err := url.ParseRequestURI(iconURL); err != nil {
|
||||
problems = append(problems, errors.New("absolute icon URL is not correctly formatted"))
|
||||
errs = append(errs, xerrors.New("absolute icon URL is not correctly formatted"))
|
||||
}
|
||||
if strings.Contains(iconURL, "?") {
|
||||
problems = append(problems, errors.New("icon URLs cannot contain query parameters"))
|
||||
errs = append(errs, xerrors.New("icon URLs cannot contain query parameters"))
|
||||
}
|
||||
return problems
|
||||
return errs
|
||||
}
|
||||
|
||||
// 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))
|
||||
// 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))
|
||||
}
|
||||
|
||||
return problems
|
||||
return errs
|
||||
}
|
||||
|
||||
func validateCoderResourceTags(tags []string) error {
|
||||
if tags == nil {
|
||||
return errors.New("provided tags array is nil")
|
||||
return xerrors.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) {
|
||||
@@ -100,21 +106,16 @@ func validateCoderResourceTags(tags []string) error {
|
||||
}
|
||||
|
||||
if len(invalidTags) != 0 {
|
||||
return fmt.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
|
||||
return xerrors.Errorf("found invalid tags (tags that cannot be used for filter state in the Registry website): [%s]", strings.Join(invalidTags, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
var errs []error
|
||||
|
||||
trimmed := strings.TrimSpace(body)
|
||||
// TODO: this may cause unexpected behavior since the errors slice may have a 0 length. Add a test.
|
||||
errs = append(errs, validateReadmeBody(trimmed)...)
|
||||
|
||||
foundParagraph := false
|
||||
@@ -130,9 +131,8 @@ 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, errors.New("all .hcl language references must be converted to .tf"))
|
||||
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -159,35 +159,33 @@ 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, errors.New("did not find Terraform code block within h1 section"))
|
||||
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
|
||||
} else {
|
||||
if terraformCodeBlockCount > 1 {
|
||||
errs = append(errs, errors.New("cannot have more than one Terraform code block in h1 section"))
|
||||
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
|
||||
}
|
||||
if !foundTerraformVersionRef {
|
||||
errs = append(errs, errors.New("did not find Terraform code block that specifies 'version' field"))
|
||||
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
|
||||
}
|
||||
}
|
||||
if !foundParagraph {
|
||||
errs = append(errs, errors.New("did not find paragraph within h1 section"))
|
||||
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
|
||||
}
|
||||
if isInsideCodeBlock {
|
||||
errs = append(errs, errors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -220,12 +218,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{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := coderResourceFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return coderResourceReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
return coderResourceReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return coderResourceReadme{
|
||||
@@ -250,21 +248,21 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
||||
}
|
||||
if len(yamlParsingErrs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlParsingErrs,
|
||||
}
|
||||
}
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, readme := range resources {
|
||||
errors := validateCoderResourceReadme(readme)
|
||||
if len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
errs := validateCoderResourceReadme(readme)
|
||||
if len(errs) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errs...)
|
||||
}
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlValidationErrors,
|
||||
}
|
||||
}
|
||||
@@ -273,8 +271,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(resources map[string]coderResourceReadme) error {
|
||||
// the body's AST.
|
||||
func validateCoderResourceRelativeURLs(_ map[string]coderResourceReadme) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -321,7 +319,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseFileLoad,
|
||||
phase: validationPhaseFile,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -330,7 +328,7 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||
|
||||
func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||
return fmt.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
||||
return xerrors.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
||||
}
|
||||
|
||||
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
||||
@@ -338,17 +336,16 @@ func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Processing %d README files\n", len(allReadmeFiles))
|
||||
logger.Info(context.Background(), "rocessing README files", "num_files", len(allReadmeFiles))
|
||||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Processed %d README files as valid Coder resources with type %q", len(resources), resourceType)
|
||||
logger.Info(context.Background(), "rocessed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
|
||||
|
||||
err = validateCoderResourceRelativeUrls(resources)
|
||||
if err != nil {
|
||||
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("All relative URLs for %s READMEs are valid\n", resourceType)
|
||||
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "type", resourceType)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"context"
|
||||
"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"`
|
||||
// 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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type contributorProfileReadme struct {
|
||||
@@ -35,7 +32,7 @@ type contributorProfileReadme struct {
|
||||
|
||||
func validateContributorDisplayName(displayName string) error {
|
||||
if displayName == "" {
|
||||
return fmt.Errorf("missing display_name")
|
||||
return xerrors.New("missing display_name")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -47,12 +44,15 @@ func validateContributorLinkedinURL(linkedinURL *string) error {
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(*linkedinURL); err != nil {
|
||||
return fmt.Errorf("linkedIn URL %q is not valid: %v", *linkedinURL, err)
|
||||
return xerrors.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,34 +60,30 @@ 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, fmt.Errorf("email address %q is missing @ symbol", *email))
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing @ symbol", *email))
|
||||
return errs
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing username", *email))
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing username", *email))
|
||||
}
|
||||
|
||||
domain, tld, ok := strings.Cut(server, ".")
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing period for server segment", *email))
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing period for server segment", *email))
|
||||
return errs
|
||||
}
|
||||
|
||||
if domain == "" {
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing domain", *email))
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing domain", *email))
|
||||
}
|
||||
if tld == "" {
|
||||
errs = append(errs, fmt.Errorf("email address %q is missing top-level domain", *email))
|
||||
errs = append(errs, xerrors.Errorf("email address %q is missing top-level domain", *email))
|
||||
}
|
||||
if strings.Contains(*email, "?") {
|
||||
errs = append(errs, errors.New("email is not allowed to contain query parameters"))
|
||||
errs = append(errs, xerrors.New("email is not allowed to contain query parameters"))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -99,7 +95,7 @@ func validateContributorWebsite(websiteURL *string) error {
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(*websiteURL); err != nil {
|
||||
return fmt.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
|
||||
return xerrors.Errorf("linkedIn URL %q is not valid: %v", *websiteURL, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -107,35 +103,32 @@ func validateContributorWebsite(websiteURL *string) error {
|
||||
|
||||
func validateContributorStatus(status string) error {
|
||||
if !slices.Contains(validContributorStatuses, status) {
|
||||
return fmt.Errorf("contributor status %q is not valid", status)
|
||||
return xerrors.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 == "" {
|
||||
errs = append(errs, errors.New("avatar URL must be omitted or non-empty string"))
|
||||
return errs
|
||||
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
|
||||
}
|
||||
|
||||
// Have to use .Parse instead of .ParseRequestURI because this is the
|
||||
// one field that's allowed to be a relative URL
|
||||
errs := []error{}
|
||||
// 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, fmt.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
||||
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
||||
}
|
||||
if strings.Contains(*avatarURL, "?") {
|
||||
errs = append(errs, errors.New("avatar URL is not allowed to contain search parameters"))
|
||||
errs = append(errs, xerrors.New("avatar URL is not allowed to contain search parameters"))
|
||||
}
|
||||
|
||||
matched := false
|
||||
var matched bool
|
||||
for _, ff := range supportedAvatarFileFormats {
|
||||
matched = strings.HasSuffix(*avatarURL, ff)
|
||||
if matched {
|
||||
@@ -145,7 +138,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
||||
if !matched {
|
||||
segments := strings.Split(*avatarURL, ".")
|
||||
fileExtension := segments[len(segments)-1]
|
||||
errs = append(errs, fmt.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
|
||||
errs = append(errs, xerrors.Errorf("avatar URL '.%s' does not end in a supported file format: [%s]", fileExtension, strings.Join(supportedAvatarFileFormats, ", ")))
|
||||
}
|
||||
|
||||
return errs
|
||||
@@ -180,12 +173,12 @@ func validateContributorReadme(rm contributorProfileReadme) []error {
|
||||
func parseContributorProfile(rm readme) (contributorProfileReadme, error) {
|
||||
fm, _, err := separateFrontmatter(rm.rawText)
|
||||
if err != nil {
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse frontmatter: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
yml := contributorProfileFrontmatter{}
|
||||
if err := yaml.Unmarshal([]byte(fm), &yml); err != nil {
|
||||
return contributorProfileReadme{}, fmt.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
return contributorProfileReadme{}, xerrors.Errorf("%q: failed to parse: %v", rm.filePath, err)
|
||||
}
|
||||
|
||||
return contributorProfileReadme{
|
||||
@@ -206,29 +199,28 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
||||
}
|
||||
|
||||
if prev, alreadyExists := profilesByNamespace[p.namespace]; alreadyExists {
|
||||
yamlParsingErrors = append(yamlParsingErrors, fmt.Errorf("%q: namespace %q conflicts with namespace from %q", p.filePath, p.namespace, prev.filePath))
|
||||
yamlParsingErrors = append(yamlParsingErrors, xerrors.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: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlParsingErrors,
|
||||
}
|
||||
}
|
||||
|
||||
yamlValidationErrors := []error{}
|
||||
for _, p := range profilesByNamespace {
|
||||
errors := validateContributorReadme(p)
|
||||
if len(errors) > 0 {
|
||||
if errors := validateContributorReadme(p); len(errors) > 0 {
|
||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(yamlValidationErrors) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseReadmeParsing,
|
||||
phase: validationPhaseReadme,
|
||||
errors: yamlValidationErrors,
|
||||
}
|
||||
}
|
||||
@@ -244,12 +236,13 @@ 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 {
|
||||
@@ -264,7 +257,7 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
||||
|
||||
if len(errs) != 0 {
|
||||
return nil, validationPhaseError{
|
||||
phase: validationPhaseFileLoad,
|
||||
phase: validationPhaseFile,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -272,34 +265,31 @@ 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
|
||||
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.
|
||||
var 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
|
||||
}
|
||||
|
||||
isRelativeURL := strings.HasPrefix(*con.frontmatter.AvatarURL, ".") ||
|
||||
strings.HasPrefix(*con.frontmatter.AvatarURL, "/")
|
||||
if !isRelativeURL {
|
||||
if !strings.HasPrefix(*con.frontmatter.AvatarURL, ".") || !strings.HasPrefix(*con.frontmatter.AvatarURL, "/") {
|
||||
continue
|
||||
}
|
||||
|
||||
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))
|
||||
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))
|
||||
continue
|
||||
}
|
||||
|
||||
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))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,7 +297,7 @@ func validateContributorRelativeUrls(contributors map[string]contributorProfileR
|
||||
return nil
|
||||
}
|
||||
return validationPhaseError{
|
||||
phase: validationPhaseAssetCrossReference,
|
||||
phase: validationPhaseCrossReference,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
@@ -318,19 +308,18 @@ func validateAllContributorFiles() error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Processing %d README files\n", len(allReadmeFiles))
|
||||
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
|
||||
contributors, err := parseContributorFiles(allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Processed %d README files as valid contributor profiles", len(contributors))
|
||||
logger.Info(context.Background(), "processed README files as valid contributor profiles", "num_contributors", len(contributors))
|
||||
|
||||
err = validateContributorRelativeUrls(contributors)
|
||||
if err != nil {
|
||||
if err := validateContributorRelativeURLs(contributors); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("All relative URLs for READMEs are valid")
|
||||
logger.Info(context.Background(), "all relative URLs for READMEs are valid")
|
||||
|
||||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
|
||||
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
// 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.
|
||||
"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.
|
||||
type validationPhaseError struct {
|
||||
phase validationPhase
|
||||
errors []error
|
||||
@@ -24,5 +26,5 @@ func (vpe validationPhaseError) Error() string {
|
||||
}
|
||||
|
||||
func addFilePathToError(filePath string, err error) error {
|
||||
return fmt.Errorf("%q: %v", filePath, err)
|
||||
return xerrors.Errorf("%q: %v", filePath, err)
|
||||
}
|
||||
|
||||
@@ -6,25 +6,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.Println("Starting README validation")
|
||||
var logger = slog.Make(sloghuman.Sink(os.Stdout))
|
||||
|
||||
// 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)
|
||||
func main() {
|
||||
logger.Info(context.Background(), "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())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
err := validateAllContributorFiles()
|
||||
err = validateAllContributorFiles()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
@@ -34,11 +37,11 @@ func main() {
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
log.Printf("Processed all READMEs in the %q directory\n", rootRegistryPath)
|
||||
logger.Info(context.Background(), "processed all READMEs in directory", "dir", rootRegistryPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
for _, err := range errs {
|
||||
fmt.Println(err)
|
||||
logger.Error(context.Background(), err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -2,35 +2,65 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
const rootRegistryPath = "./registry"
|
||||
// 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
|
||||
|
||||
var supportedAvatarFileFormats = []string{".png", ".jpeg", ".jpg", ".gif", ".svg"}
|
||||
const (
|
||||
rootRegistryPath = "./registry"
|
||||
|
||||
// readme represents a single README file within the repo (usually within the
|
||||
// top-level "/registry" directory).
|
||||
// --- 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).
|
||||
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) (string, string, error) {
|
||||
func separateFrontmatter(readmeText string) (readmeFrontmatter string, readmeBody string, err error) {
|
||||
if readmeText == "" {
|
||||
return "", "", errors.New("README is empty")
|
||||
return "", "", xerrors.New("README is empty")
|
||||
}
|
||||
|
||||
const fence = "---"
|
||||
fm := ""
|
||||
body := ""
|
||||
|
||||
var fm strings.Builder
|
||||
var body strings.Builder
|
||||
fenceCount := 0
|
||||
|
||||
lineScanner := bufio.NewScanner(strings.NewReader(strings.TrimSpace(readmeText)))
|
||||
@@ -40,48 +70,43 @@ func separateFrontmatter(readmeText string) (string, string, error) {
|
||||
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 {
|
||||
body += nextLine + "\n"
|
||||
fmt.Fprintf(&body, "%s\n", nextLine)
|
||||
} else {
|
||||
fm += strings.TrimSpace(nextLine) + "\n"
|
||||
fmt.Fprintf(&fm, "%s\n", strings.TrimSpace(nextLine))
|
||||
}
|
||||
}
|
||||
if fenceCount < 2 {
|
||||
return "", "", errors.New("README does not have two sets of frontmatter fences")
|
||||
return "", "", xerrors.New("README does not have two sets of frontmatter fences")
|
||||
}
|
||||
if fm == "" {
|
||||
return "", "", errors.New("readme has frontmatter fences but no frontmatter content")
|
||||
if fm.Len() == 0 {
|
||||
return "", "", xerrors.New("readme has frontmatter fences but no frontmatter content")
|
||||
}
|
||||
|
||||
return fm, strings.TrimSpace(body), nil
|
||||
return fm.String(), strings.TrimSpace(body.String()), nil
|
||||
}
|
||||
|
||||
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
|
||||
// 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{errors.New("README body is empty")}
|
||||
return []error{xerrors.New("README body is empty")}
|
||||
}
|
||||
|
||||
// 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 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 !strings.HasPrefix(trimmed, "# ") {
|
||||
return []error{errors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
|
||||
return []error{xerrors.New("README body must start with ATX-style h1 header (i.e., \"# \")")}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
@@ -93,9 +118,8 @@ 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
|
||||
@@ -109,9 +133,9 @@ func validateReadmeBody(body string) []error {
|
||||
continue
|
||||
}
|
||||
|
||||
spaceAfterHeader := headerGroups[2]
|
||||
if spaceAfterHeader == "" {
|
||||
errs = append(errs, errors.New("header does not have space between header characters and main header text"))
|
||||
// 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"))
|
||||
}
|
||||
|
||||
nextHeaderLevel := len(headerGroups[1])
|
||||
@@ -121,58 +145,26 @@ 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, errors.New("READMEs cannot contain more than h1 header"))
|
||||
errs = append(errs, xerrors.New("READMEs cannot contain more than h1 header"))
|
||||
break
|
||||
}
|
||||
if nextHeaderLevel > 6 {
|
||||
errs = append(errs, fmt.Errorf("README/HTML files cannot have headers exceed level 6 (found level %d)", nextHeaderLevel))
|
||||
errs = append(errs, xerrors.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, fmt.Errorf("headers are not allowed to increase more than 1 level at a time"))
|
||||
errs = append(errs, xerrors.New("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"
|
||||
)
|
||||
|
||||
@@ -2,69 +2,60 @@ 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) {
|
||||
errs = append(errs, addFilePathToError(dirPath, err))
|
||||
return []error{addFilePathToError(dirPath, err)}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
if !subDir.IsDir() {
|
||||
errs = append(errs, fmt.Errorf("%q: path is not a directory", dirPath))
|
||||
return errs
|
||||
return []error{xerrors.Errorf("%q: path is not a directory", dirPath)}
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(dirPath)
|
||||
if err != nil {
|
||||
errs = append(errs, addFilePathToError(dirPath, err))
|
||||
return errs
|
||||
return []error{addFilePathToError(dirPath, err)}
|
||||
}
|
||||
|
||||
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")
|
||||
_, err := os.Stat(resourceReadmePath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(resourceReadmePath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, fmt.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||||
errs = append(errs, xerrors.Errorf("%q: 'README.md' does not exist", resourceReadmePath))
|
||||
} else {
|
||||
errs = append(errs, addFilePathToError(resourceReadmePath, err))
|
||||
}
|
||||
}
|
||||
|
||||
mainTerraformPath := path.Join(dirPath, f.Name(), "main.tf")
|
||||
_, err = os.Stat(mainTerraformPath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(mainTerraformPath); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, fmt.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||||
errs = append(errs, xerrors.Errorf("%q: 'main.tf' file does not exist", mainTerraformPath))
|
||||
} else {
|
||||
errs = append(errs, addFilePathToError(mainTerraformPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -78,13 +69,12 @@ func validateRegistryDirectory() []error {
|
||||
for _, d := range userDirs {
|
||||
dirPath := path.Join(rootRegistryPath, d.Name())
|
||||
if !d.IsDir() {
|
||||
allErrs = append(allErrs, fmt.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
|
||||
allErrs = append(allErrs, xerrors.Errorf("detected non-directory file %q at base of main Registry directory", dirPath))
|
||||
continue
|
||||
}
|
||||
|
||||
contributorReadmePath := path.Join(dirPath, "README.md")
|
||||
_, err := os.Stat(contributorReadmePath)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(contributorReadmePath); err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
}
|
||||
|
||||
@@ -95,8 +85,7 @@ 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
|
||||
}
|
||||
@@ -105,13 +94,12 @@ func validateRegistryDirectory() []error {
|
||||
filePath := path.Join(dirPath, segment)
|
||||
|
||||
if !slices.Contains(supportedUserNameSpaceDirectories, segment) {
|
||||
allErrs = append(allErrs, fmt.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
|
||||
allErrs = append(allErrs, xerrors.Errorf("%q: only these sub-directories are allowed at top of user namespace: [%s]", filePath, strings.Join(supportedUserNameSpaceDirectories, ", ")))
|
||||
continue
|
||||
}
|
||||
|
||||
if slices.Contains(supportedResourceTypes, segment) {
|
||||
errs := validateCoderResourceSubdirectory(filePath)
|
||||
if len(errs) != 0 {
|
||||
if errs := validateCoderResourceSubdirectory(filePath); len(errs) != 0 {
|
||||
allErrs = append(allErrs, errs...)
|
||||
}
|
||||
}
|
||||
@@ -122,20 +110,19 @@ func validateRegistryDirectory() []error {
|
||||
}
|
||||
|
||||
func validateRepoStructure() error {
|
||||
var problems []error
|
||||
if errs := validateRegistryDirectory(); len(errs) != 0 {
|
||||
problems = append(problems, errs...)
|
||||
var errs []error
|
||||
if vrdErrs := validateRegistryDirectory(); len(vrdErrs) != 0 {
|
||||
errs = append(errs, vrdErrs...)
|
||||
}
|
||||
|
||||
_, 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 _, err := os.Stat("./.icons"); err != nil {
|
||||
errs = append(errs, xerrors.New("missing top-level .icons directory (used for storing reusable Coder resource icons)"))
|
||||
}
|
||||
|
||||
if len(problems) != 0 {
|
||||
if len(errs) != 0 {
|
||||
return validationPhaseError{
|
||||
phase: validationPhaseFileStructureValidation,
|
||||
errors: problems,
|
||||
phase: validationPhaseStructure,
|
||||
errors: errs,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -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.2"
|
||||
version = "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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.2"
|
||||
version = "1.0.0"
|
||||
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.2"
|
||||
version = "1.0.0"
|
||||
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.2"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,4 +105,3 @@ data "coder_parameter" "MODULE_NAME" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,25 @@ module coder.com/coder-registry
|
||||
|
||||
go 1.23.2
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
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=
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 654 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 526 KiB After Width: | Height: | Size: 428 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 85 KiB |
@@ -2,6 +2,7 @@
|
||||
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
|
||||
|
||||
@@ -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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
ai_api_key = var.anthropic_api_key
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ 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."
|
||||
@@ -210,6 +216,13 @@ EOT
|
||||
model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model"
|
||||
}
|
||||
|
||||
# Set environment variable for AI provider API key
|
||||
resource "coder_env" "ai_api_key" {
|
||||
agent_id = var.agent_id
|
||||
name = local.env_var_name
|
||||
value = var.ai_api_key
|
||||
}
|
||||
|
||||
# Install and Initialize Aider
|
||||
resource "coder_script" "aider" {
|
||||
agent_id = var.agent_id
|
||||
@@ -224,17 +237,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..."
|
||||
@@ -296,7 +309,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
|
||||
@@ -319,37 +332,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
|
||||
@@ -357,31 +370,31 @@ 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}\""
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "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."
|
||||
else
|
||||
# Configure tmux for shared sessions
|
||||
@@ -389,25 +402,25 @@ 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}\""
|
||||
tmux new-session -d -s ${var.session_name} -c ${var.folder} "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."
|
||||
fi
|
||||
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"
|
||||
@@ -417,24 +430,23 @@ 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}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
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"
|
||||
@@ -444,19 +456,18 @@ 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}
|
||||
export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\"
|
||||
export ${local.env_var_name}=\"${var.ai_api_key}\"
|
||||
aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"
|
||||
/bin/bash
|
||||
"
|
||||
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
|
||||
@@ -471,19 +482,19 @@ 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..."
|
||||
tmux attach-session -t ${var.session_name}
|
||||
else
|
||||
echo "Starting new Aider tmux session..."
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
tmux new-session -s ${var.session_name} -c ${var.folder} "aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash"
|
||||
fi
|
||||
elif [ "${var.use_screen}" = "true" ]; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; then
|
||||
@@ -494,9 +505,9 @@ resource "coder_app" "aider_cli" {
|
||||
else
|
||||
cd "${var.folder}"
|
||||
echo "Starting Aider directly..."
|
||||
export ${local.env_var_name}="${var.ai_api_key}"
|
||||
aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}"
|
||||
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.0.24"
|
||||
version = "1.1.0"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,23 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
@@ -45,6 +57,8 @@ 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" {
|
||||
|
||||
@@ -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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
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.0.1"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
experiment_auth_tarball = var.amazon_q_auth_tarball
|
||||
experiment_pre_install_script = "echo Pre-install!"
|
||||
@@ -114,25 +114,6 @@ module "amazon-q" {
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
| Name | Required | Default | Description |
|
||||
| -------------------------------- | -------- | ------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| `agent_id` | Yes | — | The ID of a Coder agent. |
|
||||
| `experiment_auth_tarball` | Yes | — | Base64-encoded, zstd-compressed tarball of a pre-authenticated Amazon Q config directory. |
|
||||
| `install_amazon_q` | No | `true` | Whether to install Amazon Q. |
|
||||
| `amazon_q_version` | No | `latest` | Version to install. |
|
||||
| `experiment_use_screen` | No | `false` | Use GNU screen for background operation. |
|
||||
| `experiment_use_tmux` | No | `false` | Use tmux for background operation. |
|
||||
| `experiment_report_tasks` | No | `false` | Enable task reporting to Coder. |
|
||||
| `experiment_pre_install_script` | No | `null` | Custom script to run before install. |
|
||||
| `experiment_post_install_script` | No | `null` | Custom script to run after install. |
|
||||
| `icon` | No | `/icon/amazon-q.svg` | The icon to use for the app. |
|
||||
| `folder` | No | `/home/coder` | The folder to run Amazon Q in. |
|
||||
| `order` | No | `null` | The order determines the position of app in the UI presentation. |
|
||||
| `system_prompt` | No | See [main.tf](./main.tf) | The system prompt to use for Amazon Q. This should instruct the agent how to do task reporting. |
|
||||
| `ai_prompt` | No | See [main.tf](./main.tf) | The initial task prompt to send to Amazon Q. |
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one of `experiment_use_screen` or `experiment_use_tmux` can be true at a time.
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ 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."
|
||||
@@ -213,7 +219,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"
|
||||
@@ -227,19 +233,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
|
||||
@@ -247,7 +253,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
|
||||
@@ -259,7 +265,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"
|
||||
@@ -271,7 +277,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
|
||||
@@ -326,4 +332,6 @@ resource "coder_app" "amazon_q" {
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Claude Code in your workspace
|
||||
icon: ../../../../.icons/claude.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, claude-code]
|
||||
tags: [agent, claude-code, ai]
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -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.2.1"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -22,6 +22,11 @@ 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
|
||||
@@ -67,34 +72,51 @@ data "coder_parameter" "ai_prompt" {
|
||||
mutable = true
|
||||
}
|
||||
|
||||
# Set the prompt and system prompt for Claude Code via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
CODER_MCP_APP_STATUS_SLUG = "claude-code"
|
||||
CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help with code.
|
||||
EOT
|
||||
}
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
claude_code_version = "0.2.57"
|
||||
|
||||
# Set API key and prompts directly in the module
|
||||
claude_api_key = var.anthropic_api_key # or use a coder_parameter
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful assistant that can help with code.
|
||||
EOT
|
||||
|
||||
# Enable experimental features
|
||||
experiment_use_screen = true # Or use experiment_use_tmux = true to use tmux instead
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
|
||||
## Session Persistence (Experimental)
|
||||
|
||||
Enable automatic session persistence to maintain Claude Code sessions across workspace restarts:
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
# Enable tmux with session persistence
|
||||
experiment_use_tmux = true
|
||||
experiment_tmux_session_persistence = true
|
||||
experiment_tmux_session_save_interval = "10" # Save every 10 minutes
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
```
|
||||
|
||||
Session persistence automatically saves and restores your Claude Code environment, including working directory and command history.
|
||||
|
||||
## Run standalone
|
||||
|
||||
Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI.
|
||||
@@ -102,7 +124,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.2.1"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ 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."
|
||||
@@ -78,11 +84,76 @@ variable "experiment_post_install_script" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "experiment_tmux_session_persistence" {
|
||||
type = bool
|
||||
description = "Whether to enable tmux session persistence across workspace restarts."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "experiment_tmux_session_save_interval" {
|
||||
type = string
|
||||
description = "How often to save tmux sessions in minutes."
|
||||
default = "15"
|
||||
}
|
||||
|
||||
variable "claude_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Claude."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt for Claude Code."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for Claude Code."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "app_status_slug" {
|
||||
type = string
|
||||
description = "App status slug for Claude Code."
|
||||
default = "claude-code"
|
||||
}
|
||||
|
||||
locals {
|
||||
encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : ""
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Set environment variables for Claude Code
|
||||
resource "coder_env" "claude_api_key" {
|
||||
count = var.claude_api_key != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.claude_api_key
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
count = var.task_prompt != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = var.task_prompt
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
count = var.system_prompt != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = var.system_prompt
|
||||
}
|
||||
|
||||
resource "coder_env" "claude_app_status_slug" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = var.app_status_slug
|
||||
}
|
||||
|
||||
# Install and Initialize Claude Code
|
||||
resource "coder_script" "claude_code" {
|
||||
agent_id = var.agent_id
|
||||
@@ -92,12 +163,36 @@ resource "coder_script" "claude_code" {
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Run pre-install script if provided
|
||||
install_tmux() {
|
||||
echo "Installing tmux..."
|
||||
if command_exists apt-get; then
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
elif command_exists yum; then
|
||||
sudo yum install -y tmux
|
||||
elif command_exists dnf; then
|
||||
sudo dnf install -y tmux
|
||||
elif command_exists pacman; then
|
||||
sudo pacman -S --noconfirm tmux
|
||||
elif command_exists apk; then
|
||||
sudo apk add tmux
|
||||
else
|
||||
echo "Error: Unable to install tmux automatically. Package manager not recognized."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
if [ -n "${local.encoded_pre_install_script}" ]; then
|
||||
echo "Running pre-install script..."
|
||||
echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh
|
||||
@@ -105,80 +200,143 @@ resource "coder_script" "claude_code" {
|
||||
/tmp/pre_install.sh
|
||||
fi
|
||||
|
||||
# Install Claude Code if enabled
|
||||
if [ "${var.install_claude_code}" = "true" ]; then
|
||||
if ! command_exists npm; then
|
||||
echo "Error: npm is not installed. Please install Node.js and npm first."
|
||||
exit 1
|
||||
echo "npm not found, checking for Node.js installation..."
|
||||
if ! command_exists node; then
|
||||
echo "Node.js not found, installing Node.js via NVM..."
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
if [ ! -d "$NVM_DIR" ]; then
|
||||
mkdir -p "$NVM_DIR"
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
else
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
|
||||
nvm install --lts
|
||||
nvm use --lts
|
||||
nvm alias default node
|
||||
|
||||
echo "Node.js installed: $(node --version)"
|
||||
echo "npm installed: $(npm --version)"
|
||||
else
|
||||
echo "Node.js is installed but npm is not available. Please install npm manually."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Installing Claude Code..."
|
||||
npm install -g @anthropic-ai/claude-code@${var.claude_code_version}
|
||||
fi
|
||||
|
||||
# Run post-install script if provided
|
||||
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 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 [ -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_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
|
||||
|
||||
# Run with tmux if enabled
|
||||
if [ "${var.experiment_tmux_session_persistence}" = "true" ] && [ "${var.experiment_use_tmux}" != "true" ]; then
|
||||
echo "Error: Session persistence requires tmux to be enabled."
|
||||
echo "Please set experiment_use_tmux = true when using session persistence."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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."
|
||||
exit 1
|
||||
install_tmux
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_tmux_session_persistence}" = "true" ]; then
|
||||
echo "Setting up tmux session persistence..."
|
||||
if ! command_exists git; then
|
||||
echo "Git not found, installing git..."
|
||||
if command_exists apt-get; then
|
||||
sudo apt-get update && sudo apt-get install -y git
|
||||
elif command_exists yum; then
|
||||
sudo yum install -y git
|
||||
elif command_exists dnf; then
|
||||
sudo dnf install -y git
|
||||
elif command_exists pacman; then
|
||||
sudo pacman -S --noconfirm git
|
||||
elif command_exists apk; then
|
||||
sudo apk add git
|
||||
else
|
||||
echo "Error: Unable to install git automatically. Package manager not recognized."
|
||||
echo "Please install git manually to enable session persistence."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p ~/.tmux/plugins
|
||||
if [ ! -d ~/.tmux/plugins/tpm ]; then
|
||||
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
|
||||
fi
|
||||
|
||||
cat > ~/.tmux.conf << EOF
|
||||
# Claude Code tmux persistence configuration
|
||||
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
||||
set -g @plugin 'tmux-plugins/tmux-continuum'
|
||||
|
||||
# Configure session persistence
|
||||
set -g @resurrect-processes ':all:'
|
||||
set -g @resurrect-capture-pane-contents 'on'
|
||||
set -g @resurrect-save-bash-history 'on'
|
||||
set -g @continuum-restore 'on'
|
||||
set -g @continuum-save-interval '${var.experiment_tmux_session_save_interval}'
|
||||
set -g @continuum-boot 'on'
|
||||
set -g @continuum-save-on 'on'
|
||||
|
||||
# Initialize plugin manager
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
EOF
|
||||
|
||||
~/.tmux/plugins/tpm/scripts/install_plugins.sh
|
||||
fi
|
||||
|
||||
echo "Running Claude Code in the background with tmux..."
|
||||
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"
|
||||
|
||||
# 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
|
||||
|
||||
if [ "${var.experiment_tmux_session_persistence}" = "true" ]; then
|
||||
sleep 3
|
||||
|
||||
if ! tmux has-session -t claude-code 2>/dev/null; then
|
||||
# Only create a new session if one doesn't exist
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
|
||||
fi
|
||||
else
|
||||
if ! tmux has-session -t claude-code 2>/dev/null; then
|
||||
tmux new-session -d -s claude-code -c ${var.folder} "claude --dangerously-skip-permissions \"$CODER_MCP_CLAUDE_TASK_PROMPT\""
|
||||
fi
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
touch "$HOME/.claude-code.log"
|
||||
|
||||
# Ensure the screenrc exists
|
||||
if [ ! -f "$HOME/.screenrc" ]; then
|
||||
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"
|
||||
@@ -188,22 +346,16 @@ resource "coder_script" "claude_code" {
|
||||
echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log"
|
||||
echo "acladd $(whoami)" >> "$HOME/.screenrc"
|
||||
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 | tee -a "$HOME/.claude-code.log"
|
||||
claude --dangerously-skip-permissions "$CODER_MCP_CLAUDE_TASK_PROMPT" | 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
|
||||
echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually."
|
||||
exit 1
|
||||
@@ -227,6 +379,10 @@ resource "coder_app" "claude_code" {
|
||||
if [ "${var.experiment_use_tmux}" = "true" ]; then
|
||||
if tmux has-session -t claude-code 2>/dev/null; then
|
||||
echo "Attaching to existing Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
# If Claude isn't running in the session, start it without the prompt
|
||||
if ! tmux list-panes -t claude-code -F '#{pane_current_command}' | grep -q "claude"; then
|
||||
tmux send-keys -t claude-code "cd ${var.folder} && claude -c --dangerously-skip-permissions" C-m
|
||||
fi
|
||||
tmux attach-session -t claude-code
|
||||
else
|
||||
echo "Starting a new Claude Code tmux session." | tee -a "$HOME/.claude-code.log"
|
||||
@@ -246,4 +402,6 @@ resource "coder_app" "claude_code" {
|
||||
fi
|
||||
EOT
|
||||
icon = var.icon
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: VS Code in the browser
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, ide, web]
|
||||
tags: [ide, web, code-server]
|
||||
---
|
||||
|
||||
# code-server
|
||||
@@ -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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.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.2.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
offline = true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.1"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,12 @@ 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"
|
||||
@@ -187,6 +193,7 @@ resource "coder_app" "code-server" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
open_in = var.open_in
|
||||
|
||||
healthcheck {
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch Cursor IDE
|
||||
icon: ../../../../.icons/cursor.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, cursor, helper]
|
||||
tags: [ide, cursor, ai]
|
||||
---
|
||||
|
||||
# Cursor IDE
|
||||
@@ -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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,12 @@ 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."
|
||||
@@ -54,6 +60,7 @@ 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=",
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Allow developers to optionally bring their own dotfiles repository
|
||||
icon: ../../../../.icons/dotfiles.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, dotfiles]
|
||||
---
|
||||
|
||||
# Dotfiles
|
||||
@@ -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.0.29"
|
||||
version = "1.1.0"
|
||||
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.0.29"
|
||||
version = "1.1.0"
|
||||
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.0.29"
|
||||
version = "1.1.0"
|
||||
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.0.29"
|
||||
version = "1.1.0"
|
||||
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.0.29"
|
||||
version = "1.1.0"
|
||||
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.0.29"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
default_dotfiles_uri = "https://github.com/coder/dotfiles"
|
||||
}
|
||||
|
||||
@@ -4,11 +4,23 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
@@ -79,6 +91,8 @@ 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,7 +4,7 @@ description: A file browser for your workspace
|
||||
icon: ../../../../.icons/filebrowser.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, filebrowser]
|
||||
tags: [filebrowser, web]
|
||||
---
|
||||
|
||||
# File Browser
|
||||
@@ -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.0.31"
|
||||
version = "1.1.0"
|
||||
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.0.31"
|
||||
version = "1.1.0"
|
||||
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.0.31"
|
||||
version = "1.1.0"
|
||||
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.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
agent_name = "main"
|
||||
subdomain = false
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,12 @@ 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."
|
||||
@@ -108,6 +114,7 @@ resource "coder_app" "filebrowser" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
@@ -120,4 +127,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ tags: [helper, git]
|
||||
|
||||
# git-commit-signing
|
||||
|
||||
> [!IMPORTANT]
|
||||
> [!IMPORTANT]
|
||||
> This module will only work with Git versions >=2.34, prior versions [do not support signing commits via SSH keys](https://lore.kernel.org/git/xmqq8rxpgwki.fsf@gitster.g/).
|
||||
|
||||
This module downloads your SSH key from Coder and uses it to sign commits with Git.
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Run Goose in your workspace
|
||||
icon: ../../../../.icons/goose.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [agent, goose]
|
||||
tags: [agent, goose, ai]
|
||||
---
|
||||
|
||||
# Goose
|
||||
@@ -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.1.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -24,14 +24,14 @@ module "goose" {
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `screen` must be installed in your workspace to run Goose in the background
|
||||
- `screen` or `tmux` 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` installed to use this.
|
||||
Your workspace must have `screen` or `tmux` installed to use the background session functionality.
|
||||
|
||||
### Run in the background and report tasks (Experimental)
|
||||
|
||||
@@ -66,41 +66,37 @@ data "coder_parameter" "ai_prompt" {
|
||||
mutable = true
|
||||
}
|
||||
|
||||
# Set the prompt and system prompt for Goose via environment variables
|
||||
resource "coder_agent" "main" {
|
||||
# ...
|
||||
env = {
|
||||
GOOSE_SYSTEM_PROMPT = <<-EOT
|
||||
You are a helpful assistant that can help write code.
|
||||
|
||||
Run all long running tasks (e.g. npm run dev) in the background and not in the foreground.
|
||||
|
||||
Periodically check in on background tasks.
|
||||
|
||||
Notify Coder of the status of the task before and after your steps.
|
||||
EOT
|
||||
GOOSE_TASK_PROMPT = data.coder_parameter.ai_prompt.value
|
||||
|
||||
# An API key is required for experiment_auto_configure
|
||||
# See https://block.github.io/goose/docs/getting-started/providers
|
||||
ANTHROPIC_API_KEY = var.anthropic_api_key # or use a coder_parameter
|
||||
}
|
||||
}
|
||||
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
goose_version = "v1.0.16"
|
||||
|
||||
# Set prompts and API key directly in the module
|
||||
system_prompt = <<-EOT
|
||||
You are a helpful assistant that can help write code.
|
||||
|
||||
Run all long running tasks (e.g. npm run dev) in the background and not in the foreground.
|
||||
|
||||
Periodically check in on background tasks.
|
||||
|
||||
Notify Coder of the status of the task before and after your steps.
|
||||
EOT
|
||||
task_prompt = data.coder_parameter.ai_prompt.value
|
||||
anthropic_api_key = var.anthropic_api_key # or use a coder_parameter
|
||||
|
||||
# Enable experimental features
|
||||
experiment_report_tasks = true
|
||||
|
||||
# Run Goose in the background
|
||||
# Run Goose in the background with screen (pick one: screen or tmux)
|
||||
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
|
||||
@@ -143,12 +139,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 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 tmux, and without any task reporting to the Coder UI.
|
||||
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ 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."
|
||||
@@ -54,6 +60,18 @@ 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."
|
||||
@@ -69,13 +87,13 @@ variable "experiment_auto_configure" {
|
||||
variable "experiment_goose_provider" {
|
||||
type = string
|
||||
description = "The provider to use for Goose (e.g., anthropic)."
|
||||
default = null
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_goose_model" {
|
||||
type = string
|
||||
description = "The model to use for Goose (e.g., claude-3-5-sonnet-latest)."
|
||||
default = null
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "experiment_pre_install_script" {
|
||||
@@ -96,6 +114,25 @@ variable "experiment_additional_extensions" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "system_prompt" {
|
||||
type = string
|
||||
description = "System prompt for Goose."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "task_prompt" {
|
||||
type = string
|
||||
description = "Task prompt for Goose."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Anthropic API key for Goose."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
locals {
|
||||
base_extensions = <<-EOT
|
||||
coder:
|
||||
@@ -132,6 +169,28 @@ EOT
|
||||
encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : ""
|
||||
}
|
||||
|
||||
# Set environment variables for Goose
|
||||
resource "coder_env" "goose_system_prompt" {
|
||||
count = var.system_prompt != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "GOOSE_SYSTEM_PROMPT"
|
||||
value = var.system_prompt
|
||||
}
|
||||
|
||||
resource "coder_env" "goose_task_prompt" {
|
||||
count = var.task_prompt != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "GOOSE_TASK_PROMPT"
|
||||
value = var.task_prompt
|
||||
}
|
||||
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
count = var.anthropic_api_key != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
name = "ANTHROPIC_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
}
|
||||
|
||||
# Install and Initialize Goose
|
||||
resource "coder_script" "goose" {
|
||||
agent_id = var.agent_id
|
||||
@@ -182,15 +241,59 @@ 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"
|
||||
|
||||
# Run with screen if enabled
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
|
||||
# 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
|
||||
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."
|
||||
@@ -204,7 +307,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"
|
||||
@@ -216,32 +319,12 @@ EOL
|
||||
fi
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
# 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 "
|
||||
|
||||
screen -U -dmS ${var.session_name} 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
|
||||
@@ -270,20 +353,31 @@ resource "coder_app" "goose" {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${var.experiment_use_screen}" = "true" ]; then
|
||||
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
|
||||
# Check if session exists first
|
||||
if ! screen -list | grep -q "goose"; then
|
||||
if ! screen -list | grep -q "${var.session_name}"; 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 goose
|
||||
screen -xRR ${var.session_name}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, hashicorp, hvs]
|
||||
tags: [integration, vault, hashicorp, hvs]
|
||||
---
|
||||
|
||||
# HCP Vault Secrets
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch JetBrains Gateway IDEs in the dash
|
||||
icon: ../../../../.icons/gateway.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, jetbrains, helper, parameter]
|
||||
tags: [ide, jetbrains, parameter, gateway]
|
||||
---
|
||||
|
||||
# JetBrains Gateway
|
||||
@@ -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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.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.1.0"
|
||||
version = "1.2.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 = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
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,6 +62,12 @@ 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)."
|
||||
@@ -324,6 +330,7 @@ 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,
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/jfrog.svg
|
||||
maintainer_github: coder
|
||||
partner_github: jfrog
|
||||
verified: true
|
||||
tags: [integration, jfrog]
|
||||
tags: [integration, jfrog, helper]
|
||||
---
|
||||
|
||||
# JFrog
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A module that adds Jupyter Notebook in your Coder template.
|
||||
icon: ../../../../.icons/jupyter.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [jupyter, helper, ide, web]
|
||||
tags: [jupyter, ide, web]
|
||||
---
|
||||
|
||||
# Jupyter Notebook
|
||||
@@ -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.0.19"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,12 @@ 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"
|
||||
@@ -62,4 +68,5 @@ resource "coder_app" "jupyter-notebook" {
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A module that adds JupyterLab in your Coder template.
|
||||
icon: ../../../../.icons/jupyter.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [jupyter, helper, ide, web]
|
||||
tags: [jupyter, ide, web]
|
||||
---
|
||||
|
||||
# JupyterLab
|
||||
@@ -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.0.31"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,12 @@ 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"
|
||||
@@ -72,4 +78,5 @@ resource "coder_app" "jupyterlab" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: A modern open source VNC server
|
||||
icon: ../../../../.icons/kasmvnc.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, vnc, desktop]
|
||||
tags: [vnc, desktop, kasmvnc]
|
||||
---
|
||||
|
||||
# KasmVNC
|
||||
@@ -15,9 +15,10 @@ 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.0.23"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
desktop_environment = "xfce"
|
||||
subdomain = true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.12"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,32 +29,56 @@ 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"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
PORT : var.port,
|
||||
DESKTOP_ENVIRONMENT : var.desktop_environment,
|
||||
KASM_VERSION : var.kasm_version
|
||||
})
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
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 = true
|
||||
subdomain = var.subdomain
|
||||
share = "owner"
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = "http://localhost:${var.port}/app"
|
||||
interval = 5
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<!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>
|
||||
@@ -3,6 +3,8 @@
|
||||
# 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
|
||||
@@ -188,7 +190,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!"
|
||||
|
||||
@@ -206,6 +208,7 @@ 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
|
||||
@@ -220,16 +223,82 @@ 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=$!
|
||||
|
||||
# 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"
|
||||
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
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "🚀 KasmVNC server started successfully!\n"
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
display_name: Windows RDP Desktop
|
||||
description: Enable RDP on Windows and add a one-click Coder Desktop button for seamless access
|
||||
icon: ../../../../.icons/desktop.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
supported_os: [windows]
|
||||
tags: [rdp, windows, desktop, remote]
|
||||
---
|
||||
|
||||
# Windows RDP Desktop
|
||||
|
||||
This module enables Remote Desktop Protocol (RDP) on Windows workspaces and adds a one-click button to launch RDP sessions directly through [Coder Desktop](https://coder.com/docs/user-guides/desktop). It provides a complete, standalone solution for RDP access, eliminating the need for manual configuration or port forwarding through the Coder CLI.
|
||||
|
||||
> **Note**: [Coder Desktop](https://coder.com/docs/user-guides/desktop) is required on client devices to use the Local Windows RDP access feature.
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Standalone Solution**: Automatically configures RDP on Windows workspaces
|
||||
- ✅ **One-click Access**: Launch RDP sessions directly through Coder Desktop
|
||||
- ✅ **No Port Forwarding**: Uses Coder Desktop URI handling
|
||||
- ✅ **Auto-configuration**: Sets up Windows firewall, services, and authentication
|
||||
- ✅ **Secure**: Configurable credentials with sensitive variable handling
|
||||
- ✅ **Customizable**: Display name, credentials, and UI ordering options
|
||||
|
||||
## What This Module Does
|
||||
|
||||
1. **Enables RDP** on the Windows workspace
|
||||
2. **Sets the administrator password** for RDP authentication
|
||||
3. **Configures Windows Firewall** to allow RDP connections
|
||||
4. **Starts RDP services** automatically
|
||||
5. **Creates a Coder Desktop button** for one-click access
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Uses default credentials (Username: `Administrator`, Password: `coderRDP!`):
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
agent_name = coder_agent.main.name
|
||||
}
|
||||
```
|
||||
|
||||
### Custom display name
|
||||
|
||||
Specify a custom display name for the `coder_app` button:
|
||||
|
||||
```tf
|
||||
module "rdp_desktop" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/local-windows-rdp/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.windows.id
|
||||
agent_name = "windows"
|
||||
display_name = "Windows Desktop"
|
||||
order = 1
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# PowerShell script to configure RDP for Coder Desktop access
|
||||
# This script enables RDP, sets the admin password, and configures necessary settings
|
||||
|
||||
Write-Output "[Coder RDP Setup] Starting RDP configuration..."
|
||||
|
||||
# Function to set the administrator password
|
||||
function Set-AdminPassword {
|
||||
param (
|
||||
[string]$adminUsername,
|
||||
[string]$adminPassword
|
||||
)
|
||||
|
||||
Write-Output "[Coder RDP Setup] Setting password for user: $adminUsername"
|
||||
|
||||
try {
|
||||
# Convert password to secure string
|
||||
$securePassword = ConvertTo-SecureString -AsPlainText $adminPassword -Force
|
||||
|
||||
# Set the password for the user
|
||||
Get-LocalUser -Name $adminUsername | Set-LocalUser -Password $securePassword
|
||||
|
||||
# Enable the user account (in case it's disabled)
|
||||
Get-LocalUser -Name $adminUsername | Enable-LocalUser
|
||||
|
||||
Write-Output "[Coder RDP Setup] Successfully set password for $adminUsername"
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] Failed to set password: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Function to enable and configure RDP
|
||||
function Enable-RDP {
|
||||
Write-Output "[Coder RDP Setup] Enabling Remote Desktop..."
|
||||
|
||||
try {
|
||||
# Enable RDP
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 -Force
|
||||
|
||||
# Disable Network Level Authentication (NLA) for easier access
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "UserAuthentication" -Value 0 -Force
|
||||
|
||||
# Set security layer to RDP Security Layer
|
||||
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name "SecurityLayer" -Value 1 -Force
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP enabled successfully"
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] Failed to enable RDP: $_"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Function to configure Windows Firewall for RDP
|
||||
function Configure-Firewall {
|
||||
Write-Output "[Coder RDP Setup] Configuring Windows Firewall for RDP..."
|
||||
|
||||
try {
|
||||
# Enable RDP firewall rules
|
||||
Enable-NetFirewallRule -DisplayGroup "Remote Desktop" -ErrorAction SilentlyContinue
|
||||
|
||||
# If the above fails, try alternative method
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
|
||||
}
|
||||
|
||||
Write-Output "[Coder RDP Setup] Firewall configured successfully"
|
||||
} catch {
|
||||
Write-Warning "[Coder RDP Setup] Failed to configure firewall rules: $_"
|
||||
# Continue anyway as RDP might still work
|
||||
}
|
||||
}
|
||||
|
||||
# Function to ensure RDP service is running
|
||||
function Start-RDPService {
|
||||
Write-Output "[Coder RDP Setup] Starting Remote Desktop Services..."
|
||||
|
||||
try {
|
||||
# Start the Terminal Services
|
||||
Set-Service -Name "TermService" -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service -Name "TermService" -ErrorAction SilentlyContinue
|
||||
|
||||
# Start Remote Desktop Services UserMode Port Redirector
|
||||
Set-Service -Name "UmRdpService" -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
Start-Service -Name "UmRdpService" -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP services started successfully"
|
||||
} catch {
|
||||
Write-Warning "[Coder RDP Setup] Some RDP services may not have started: $_"
|
||||
# Continue anyway
|
||||
}
|
||||
}
|
||||
|
||||
# Main execution
|
||||
try {
|
||||
# Template variables from Terraform
|
||||
$username = "${username}"
|
||||
$password = "${password}"
|
||||
|
||||
# Validate inputs
|
||||
if ([string]::IsNullOrWhiteSpace($username) -or [string]::IsNullOrWhiteSpace($password)) {
|
||||
Write-Error "[Coder RDP Setup] Username or password is empty"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Execute configuration steps
|
||||
Set-AdminPassword -adminUsername $username -adminPassword $password
|
||||
Enable-RDP
|
||||
Configure-Firewall
|
||||
Start-RDPService
|
||||
|
||||
Write-Output "[Coder RDP Setup] RDP configuration completed successfully!"
|
||||
Write-Output "[Coder RDP Setup] You can now connect using:"
|
||||
Write-Output " Username: $username"
|
||||
Write-Output " Password: [hidden]"
|
||||
Write-Output " Port: 3389 (default)"
|
||||
|
||||
} catch {
|
||||
Write-Error "[Coder RDP Setup] An unexpected error occurred: $_"
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
type TerraformState,
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
type TestVariables = Readonly<{
|
||||
agent_id: string;
|
||||
agent_name: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
display_name?: string;
|
||||
order?: number;
|
||||
}>;
|
||||
|
||||
function findRdpApp(state: TerraformState) {
|
||||
for (const resource of state.resources) {
|
||||
const isRdpAppResource =
|
||||
resource.type === "coder_app" && resource.name === "rdp_desktop";
|
||||
|
||||
if (!isRdpAppResource) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const instance of resource.instances) {
|
||||
if (instance.attributes.slug === "rdp-desktop") {
|
||||
return instance.attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findRdpScript(state: TerraformState) {
|
||||
for (const resource of state.resources) {
|
||||
const isRdpScriptResource =
|
||||
resource.type === "coder_script" && resource.name === "rdp_setup";
|
||||
|
||||
if (!isRdpScriptResource) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const instance of resource.instances) {
|
||||
if (instance.attributes.display_name === "Configure RDP") {
|
||||
return instance.attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
describe("local-windows-rdp", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "test-agent",
|
||||
});
|
||||
|
||||
it("should create RDP app with default values", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify the app was created
|
||||
expect(app).not.toBeNull();
|
||||
expect(app?.slug).toBe("rdp-desktop");
|
||||
expect(app?.display_name).toBe("RDP Desktop");
|
||||
expect(app?.icon).toBe("/icon/desktop.svg");
|
||||
expect(app?.external).toBe(true);
|
||||
|
||||
// Verify the URI format
|
||||
expect(app?.url).toStartWith("coder://");
|
||||
expect(app?.url).toContain("/v0/open/ws/");
|
||||
expect(app?.url).toContain("/agent/main/rdp");
|
||||
expect(app?.url).toContain("username=Administrator");
|
||||
expect(app?.url).toContain("password=coderRDP!");
|
||||
});
|
||||
|
||||
it("should create RDP configuration script", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const script = findRdpScript(state);
|
||||
|
||||
// Verify the script was created
|
||||
expect(script).not.toBeNull();
|
||||
expect(script?.display_name).toBe("Configure RDP");
|
||||
expect(script?.icon).toBe("/icon/desktop.svg");
|
||||
expect(script?.run_on_start).toBe(true);
|
||||
expect(script?.run_on_stop).toBe(false);
|
||||
|
||||
// Verify the script contains PowerShell configuration
|
||||
expect(script?.script).toContain("Set-AdminPassword");
|
||||
expect(script?.script).toContain("Enable-RDP");
|
||||
expect(script?.script).toContain("Configure-Firewall");
|
||||
expect(script?.script).toContain("Start-RDPService");
|
||||
});
|
||||
|
||||
it("should create RDP app with custom values", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "custom-agent-id",
|
||||
agent_name: "windows-agent",
|
||||
username: "CustomUser",
|
||||
password: "CustomPass123!",
|
||||
display_name: "Custom RDP",
|
||||
order: 5,
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify custom values
|
||||
expect(app?.display_name).toBe("Custom RDP");
|
||||
expect(app?.order).toBe(5);
|
||||
|
||||
// Verify custom credentials in URI
|
||||
expect(app?.url).toContain("/agent/windows-agent/rdp");
|
||||
expect(app?.url).toContain("username=CustomUser");
|
||||
expect(app?.url).toContain("password=CustomPass123!");
|
||||
});
|
||||
|
||||
it("should pass custom credentials to PowerShell script", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
username: "TestAdmin",
|
||||
password: "TestPassword123!",
|
||||
});
|
||||
|
||||
const script = findRdpScript(state);
|
||||
|
||||
// Verify custom credentials are in the script
|
||||
expect(script?.script).toContain('$username = "TestAdmin"');
|
||||
expect(script?.script).toContain('$password = "TestPassword123!"');
|
||||
});
|
||||
|
||||
it("should handle sensitive password variable", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
password: "SensitivePass123!",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify password is included in URI even when sensitive
|
||||
expect(app?.url).toContain("password=SensitivePass123!");
|
||||
});
|
||||
|
||||
it("should use correct default agent name", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "main",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
expect(app?.url).toContain("/agent/main/rdp");
|
||||
});
|
||||
|
||||
it("should construct proper Coder URI format", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "test-agent-id",
|
||||
agent_name: "test-agent",
|
||||
username: "TestUser",
|
||||
password: "TestPass",
|
||||
});
|
||||
|
||||
const app = findRdpApp(state);
|
||||
|
||||
// Verify complete URI structure
|
||||
expect(app?.url).toMatch(
|
||||
/^coder:\/\/[^\/]+\/v0\/open\/ws\/[^\/]+\/agent\/test-agent\/rdp\?username=TestUser&password=TestPass$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "agent_name" {
|
||||
type = string
|
||||
description = "The name of the Coder agent."
|
||||
}
|
||||
|
||||
variable "username" {
|
||||
type = string
|
||||
description = "The username for RDP authentication."
|
||||
default = "Administrator"
|
||||
}
|
||||
|
||||
variable "password" {
|
||||
type = string
|
||||
description = "The password for RDP authentication."
|
||||
default = "coderRDP!"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name for the RDP app button."
|
||||
default = "RDP Desktop"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
locals {
|
||||
# Extract server name from workspace access URL
|
||||
server_name = regex("https?:\\/\\/([^\\/]+)", data.coder_workspace.me.access_url)[0]
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
resource "coder_script" "rdp_setup" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Configure RDP"
|
||||
icon = "/icon/desktop.svg"
|
||||
script = templatefile("${path.module}/configure-rdp.ps1", {
|
||||
username = var.username
|
||||
password = var.password
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "rdp_desktop" {
|
||||
agent_id = var.agent_id
|
||||
slug = "rdp-desktop"
|
||||
display_name = var.display_name
|
||||
url = "coder://${local.server_name}/v0/open/ws/${data.coder_workspace.me.name}/agent/${var.agent_name}/rdp?username=${var.username}&password=${var.password}"
|
||||
icon = "/icon/desktop.svg"
|
||||
external = true
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Allow developers to customize their workspace on start
|
||||
icon: ../../../../.icons/personalize.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, personalize]
|
||||
---
|
||||
|
||||
# Personalize
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Send a Slack message when a command finishes inside a workspace!
|
||||
icon: ../../../../.icons/slack.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper]
|
||||
tags: [helper, slack]
|
||||
---
|
||||
|
||||
# Slack Me
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, github]
|
||||
tags: [hashicorp, integration, vault, github]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (GitHub)
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, jwt, oidc]
|
||||
tags: [hashicorp, integration, vault, jwt, oidc]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (JWT)
|
||||
@@ -109,7 +109,7 @@ resource "jwt_signed_token" "vault" {
|
||||
sub = "${data.coder_workspace.me.id}"
|
||||
aud = "https://vault.example.com"
|
||||
iat = provider::time::rfc3339_parse(plantimestamp()).unix
|
||||
# Uncomment to set an expiry on the JWT token(default 3600 seconds).
|
||||
# Uncomment to set an expiry on the JWT token(default 3600 seconds).
|
||||
# workspace will need to be restarted to generate a new token if it expires
|
||||
#exp = provider::time::rfc3339_parse(timeadd(timestamp(), 3600)).unix agent = coder_agent.main.id
|
||||
provisioner = data.coder_provisioner.main.id
|
||||
|
||||
@@ -5,7 +5,7 @@ icon: ../../../../.icons/vault.svg
|
||||
maintainer_github: coder
|
||||
partner_github: hashicorp
|
||||
verified: true
|
||||
tags: [helper, integration, vault, token]
|
||||
tags: [hashicorp, integration, vault, token]
|
||||
---
|
||||
|
||||
# Hashicorp Vault Integration (Token)
|
||||
@@ -20,11 +20,12 @@ variable "vault_token" {
|
||||
}
|
||||
|
||||
module "vault" {
|
||||
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"
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
@@ -74,7 +75,7 @@ variable "vault_token" {
|
||||
|
||||
module "vault" {
|
||||
source = "registry.coder.com/coder/vault-token/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.example.id
|
||||
vault_addr = "https://vault.example.com"
|
||||
vault_token = var.token
|
||||
|
||||
@@ -26,6 +26,11 @@ 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
|
||||
@@ -62,3 +67,10 @@ 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
|
||||
}
|
||||
@@ -4,7 +4,7 @@ description: Add a one-click button to launch VS Code Desktop
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [ide, vscode, helper]
|
||||
tags: [ide, vscode]
|
||||
---
|
||||
|
||||
# VS Code Desktop
|
||||
@@ -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.0.15"
|
||||
version = "1.1.0"
|
||||
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.0.15"
|
||||
version = "1.1.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.23"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,12 @@ 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" {}
|
||||
|
||||
@@ -42,6 +48,8 @@ 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=",
|
||||
|
||||
@@ -4,7 +4,7 @@ description: VS Code Web - Visual Studio Code in the browser
|
||||
icon: ../../../../.icons/code.svg
|
||||
maintainer_github: coder
|
||||
verified: true
|
||||
tags: [helper, ide, vscode, web]
|
||||
tags: [ide, vscode, web]
|
||||
---
|
||||
|
||||
# VS Code Web
|
||||
@@ -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.0.30"
|
||||
version = "1.3.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.0.30"
|
||||
version = "1.3.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.0.30"
|
||||
version = "1.3.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.0.30"
|
||||
version = "1.3.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.0.30"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
|
||||
@@ -4,7 +4,7 @@ terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 0.17"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,6 +97,12 @@ 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."
|
||||
@@ -115,6 +121,12 @@ variable "use_cached" {
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "disable_trust" {
|
||||
type = bool
|
||||
description = "Disables workspace trust protection for VS Code Web."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "extensions_dir" {
|
||||
type = string
|
||||
description = "Override the directory to store extensions in."
|
||||
@@ -136,6 +148,16 @@ variable "subdomain" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "platform" {
|
||||
type = string
|
||||
description = "The platform to use for the VS Code Web."
|
||||
default = ""
|
||||
validation {
|
||||
condition = var.platform == "" || var.platform == "linux" || var.platform == "darwin" || var.platform == "alpine" || var.platform == "win32"
|
||||
error_message = "Incorrect value. Please set either 'linux', 'darwin', or 'alpine' or 'win32'."
|
||||
}
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
@@ -153,11 +175,13 @@ resource "coder_script" "vscode-web" {
|
||||
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
|
||||
OFFLINE : var.offline,
|
||||
USE_CACHED : var.use_cached,
|
||||
DISABLE_TRUST : var.disable_trust,
|
||||
EXTENSIONS_DIR : var.extensions_dir,
|
||||
FOLDER : var.folder,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
SERVER_BASE_PATH : local.server_base_path,
|
||||
COMMIT_ID : var.commit_id,
|
||||
PLATFORM : var.platform,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
@@ -183,6 +207,7 @@ resource "coder_app" "vscode-web" {
|
||||
subdomain = var.subdomain
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
|
||||
healthcheck {
|
||||
url = local.healthcheck_url
|
||||
|
||||
@@ -16,10 +16,16 @@ if [ -n "${SERVER_BASE_PATH}" ]; then
|
||||
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
|
||||
fi
|
||||
|
||||
# Set disable workspace trust
|
||||
DISABLE_TRUST_ARG=""
|
||||
if [ "${DISABLE_TRUST}" = true ]; then
|
||||
DISABLE_TRUST_ARG="--disable-workspace-trust"
|
||||
fi
|
||||
|
||||
run_vscode_web() {
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
@@ -59,15 +65,26 @@ case "$ARCH" in
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect the platform
|
||||
if [ -n "${PLATFORM}" ]; then
|
||||
DETECTED_PLATFORM="${PLATFORM}"
|
||||
elif [ -f /etc/alpine-release ] || grep -qi 'ID=alpine' /etc/os-release 2>/dev/null || command -v apk > /dev/null 2>&1; then
|
||||
DETECTED_PLATFORM="alpine"
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
DETECTED_PLATFORM="darwin"
|
||||
else
|
||||
DETECTED_PLATFORM="linux"
|
||||
fi
|
||||
|
||||
# Check if a specific VS Code Web commit ID was provided
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
HASH="${COMMIT_ID}"
|
||||
else
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-linux-$ARCH-web | cut -d '"' -f 2)
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-$DETECTED_PLATFORM-$ARCH-web | cut -d '"' -f 2)
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web commit id version $HASH.\n"
|
||||
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-linux-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-$DETECTED_PLATFORM-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Microsoft Visual Studio Code Server: $output"
|
||||
|
||||
@@ -16,7 +16,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
@@ -34,7 +34,7 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
}
|
||||
@@ -46,12 +46,25 @@ module "windows_rdp" {
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.0.18"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.google_compute_instance.dev[0].id
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Devolutions Gateway Version
|
||||
|
||||
```tf
|
||||
module "windows_rdp" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windows-rdp/coder"
|
||||
version = "1.2.1"
|
||||
agent_id = resource.coder_agent.main.id
|
||||
resource_id = resource.aws_instance.dev.id
|
||||
devolutions_gateway_version = "2025.1.6" # Specify a specific version
|
||||
}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Test on Microsoft Azure.
|
||||
|
||||