chore: add validation for Coder Template README files (#326)
Closes #194 alongside #325 ## Description This PR adds the missing base layer of validation for all Coder template README files, ensuring that they all follow a consistent structure when processed by the Registry website's build step. It also updates a few README files to match the new standards. ## Type of Change - [ ] New module - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [x] Other ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally
This commit is contained in:
parent
fd074a5643
commit
08ed594bfd
143
cmd/readmevalidation/codermodules.go
Normal file
143
cmd/readmevalidation/codermodules.go
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateCoderModuleReadmeBody(body string) []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
trimmed := strings.TrimSpace(body)
|
||||||
|
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
|
||||||
|
errs = append(errs, baseErrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundParagraph := false
|
||||||
|
terraformCodeBlockCount := 0
|
||||||
|
foundTerraformVersionRef := false
|
||||||
|
|
||||||
|
lineNum := 0
|
||||||
|
isInsideCodeBlock := false
|
||||||
|
isInsideTerraform := false
|
||||||
|
|
||||||
|
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||||
|
for lineScanner.Scan() {
|
||||||
|
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.
|
||||||
|
if lineNum == 1 {
|
||||||
|
if !strings.HasPrefix(nextLine, "# ") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(nextLine, "```") {
|
||||||
|
isInsideCodeBlock = !isInsideCodeBlock
|
||||||
|
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
|
||||||
|
if isInsideTerraform {
|
||||||
|
terraformCodeBlockCount++
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(nextLine, "```hcl") {
|
||||||
|
errs = append(errs, xerrors.New("all hcl code blocks must be converted to tf"))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isInsideCodeBlock {
|
||||||
|
if isInsideTerraform {
|
||||||
|
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
trimmedLine := strings.TrimSpace(nextLine)
|
||||||
|
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
|
||||||
|
foundParagraph = foundParagraph || isParagraph
|
||||||
|
}
|
||||||
|
|
||||||
|
if terraformCodeBlockCount == 0 {
|
||||||
|
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
|
||||||
|
} else {
|
||||||
|
if terraformCodeBlockCount > 1 {
|
||||||
|
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
|
||||||
|
}
|
||||||
|
if !foundTerraformVersionRef {
|
||||||
|
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundParagraph {
|
||||||
|
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
|
||||||
|
}
|
||||||
|
if isInsideCodeBlock {
|
||||||
|
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCoderModuleReadme(rm coderResourceReadme) []error {
|
||||||
|
var errs []error
|
||||||
|
for _, err := range validateCoderModuleReadmeBody(rm.body) {
|
||||||
|
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||||
|
}
|
||||||
|
if fmErrs := validateCoderResourceFrontmatter("modules", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
|
||||||
|
errs = append(errs, fmErrs...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllCoderModuleReadmes(resources []coderResourceReadme) error {
|
||||||
|
var yamlValidationErrors []error
|
||||||
|
for _, readme := range resources {
|
||||||
|
errs := validateCoderModuleReadme(readme)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
yamlValidationErrors = append(yamlValidationErrors, errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(yamlValidationErrors) != 0 {
|
||||||
|
return validationPhaseError{
|
||||||
|
phase: validationPhaseReadme,
|
||||||
|
errors: yamlValidationErrors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllCoderModules() error {
|
||||||
|
const resourceType = "modules"
|
||||||
|
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
|
||||||
|
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = validateAllCoderModuleReadmes(resources)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
|
||||||
|
|
||||||
|
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ func TestValidateCoderResourceReadmeBody(t *testing.T) {
|
|||||||
t.Run("Parses a valid README body with zero issues", func(t *testing.T) {
|
t.Run("Parses a valid README body with zero issues", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
errs := validateCoderResourceReadmeBody(testBody)
|
errs := validateCoderModuleReadmeBody(testBody)
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
t.Error(e)
|
t.Error(e)
|
||||||
}
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -89,7 +87,7 @@ func validateCoderResourceIconURL(iconURL string) []error {
|
|||||||
return []error{xerrors.New("icon URL cannot be empty")}
|
return []error{xerrors.New("icon URL cannot be empty")}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := []error{}
|
var errs []error
|
||||||
|
|
||||||
// If the URL does not have a relative path.
|
// If the URL does not have a relative path.
|
||||||
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
|
if !strings.HasPrefix(iconURL, ".") && !strings.HasPrefix(iconURL, "/") {
|
||||||
@ -120,7 +118,7 @@ func validateCoderResourceTags(tags []string) error {
|
|||||||
|
|
||||||
// All of these tags are used for the module/template filter controls in the Registry site. Need to make sure they
|
// 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.
|
// can all be placed in the browser URL without issue.
|
||||||
invalidTags := []string{}
|
var invalidTags []string
|
||||||
for _, t := range tags {
|
for _, t := range tags {
|
||||||
if t != url.QueryEscape(t) {
|
if t != url.QueryEscape(t) {
|
||||||
invalidTags = append(invalidTags, t)
|
invalidTags = append(invalidTags, t)
|
||||||
@ -133,108 +131,27 @@ func validateCoderResourceTags(tags []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCoderResourceReadmeBody(body string) []error {
|
func validateCoderResourceFrontmatter(resourceType string, filePath string, fm coderResourceFrontmatter) []error {
|
||||||
|
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||||
|
return []error{xerrors.Errorf("cannot process unknown resource type %q", resourceType)}
|
||||||
|
}
|
||||||
|
|
||||||
var errs []error
|
var errs []error
|
||||||
|
if err := validateCoderResourceDisplayName(fm.DisplayName); err != nil {
|
||||||
trimmed := strings.TrimSpace(body)
|
errs = append(errs, addFilePathToError(filePath, err))
|
||||||
// TODO: this may cause unexpected behavior since the errors slice may have a 0 length. Add a test.
|
}
|
||||||
errs = append(errs, validateReadmeBody(trimmed)...)
|
if err := validateCoderResourceDescription(fm.Description); err != nil {
|
||||||
|
errs = append(errs, addFilePathToError(filePath, err))
|
||||||
foundParagraph := false
|
}
|
||||||
terraformCodeBlockCount := 0
|
if err := validateCoderResourceTags(fm.Tags); err != nil {
|
||||||
foundTerraformVersionRef := false
|
errs = append(errs, addFilePathToError(filePath, err))
|
||||||
|
|
||||||
lineNum := 0
|
|
||||||
isInsideCodeBlock := false
|
|
||||||
isInsideTerraform := false
|
|
||||||
|
|
||||||
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
|
||||||
for lineScanner.Scan() {
|
|
||||||
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.
|
|
||||||
if lineNum == 1 {
|
|
||||||
if !strings.HasPrefix(nextLine, "# ") {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(nextLine, "```") {
|
|
||||||
isInsideCodeBlock = !isInsideCodeBlock
|
|
||||||
isInsideTerraform = isInsideCodeBlock && strings.HasPrefix(nextLine, "```tf")
|
|
||||||
if isInsideTerraform {
|
|
||||||
terraformCodeBlockCount++
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(nextLine, "```hcl") {
|
|
||||||
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if isInsideCodeBlock {
|
|
||||||
if isInsideTerraform {
|
|
||||||
foundTerraformVersionRef = foundTerraformVersionRef || terraformVersionRe.MatchString(nextLine)
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
trimmedLine := strings.TrimSpace(nextLine)
|
|
||||||
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
|
|
||||||
foundParagraph = foundParagraph || isParagraph
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if terraformCodeBlockCount == 0 {
|
for _, err := range validateCoderResourceIconURL(fm.IconURL) {
|
||||||
errs = append(errs, xerrors.New("did not find Terraform code block within h1 section"))
|
errs = append(errs, addFilePathToError(filePath, err))
|
||||||
} else {
|
|
||||||
if terraformCodeBlockCount > 1 {
|
|
||||||
errs = append(errs, xerrors.New("cannot have more than one Terraform code block in h1 section"))
|
|
||||||
}
|
|
||||||
if !foundTerraformVersionRef {
|
|
||||||
errs = append(errs, xerrors.New("did not find Terraform code block that specifies 'version' field"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !foundParagraph {
|
for _, err := range validateSupportedOperatingSystems(fm.OperatingSystems) {
|
||||||
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
|
errs = append(errs, addFilePathToError(filePath, err))
|
||||||
}
|
|
||||||
if isInsideCodeBlock {
|
|
||||||
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateCoderResourceReadme(rm coderResourceReadme) []error {
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
for _, err := range validateCoderResourceReadmeBody(rm.body) {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateCoderResourceDisplayName(rm.frontmatter.DisplayName); err != nil {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
|
||||||
if err := validateCoderResourceDescription(rm.frontmatter.Description); err != nil {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
|
||||||
if err := validateCoderResourceTags(rm.frontmatter.Tags); err != nil {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, err := range validateCoderResourceIconURL(rm.frontmatter.IconURL) {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
|
||||||
for _, err := range validateSupportedOperatingSystems(rm.frontmatter.OperatingSystems) {
|
|
||||||
errs = append(errs, addFilePathToError(rm.filePath, err))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
@ -248,7 +165,7 @@ func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceRead
|
|||||||
|
|
||||||
keyErrs := validateFrontmatterYamlKeys(fm, supportedCoderResourceStructKeys)
|
keyErrs := validateFrontmatterYamlKeys(fm, supportedCoderResourceStructKeys)
|
||||||
if len(keyErrs) != 0 {
|
if len(keyErrs) != 0 {
|
||||||
remapped := []error{}
|
var remapped []error
|
||||||
for _, e := range keyErrs {
|
for _, e := range keyErrs {
|
||||||
remapped = append(remapped, addFilePathToError(rm.filePath, e))
|
remapped = append(remapped, addFilePathToError(rm.filePath, e))
|
||||||
}
|
}
|
||||||
@ -268,7 +185,11 @@ func parseCoderResourceReadme(resourceType string, rm readme) (coderResourceRead
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[string]coderResourceReadme, error) {
|
func parseCoderResourceReadmeFiles(resourceType string, rms []readme) ([]coderResourceReadme, error) {
|
||||||
|
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||||
|
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
resources := map[string]coderResourceReadme{}
|
resources := map[string]coderResourceReadme{}
|
||||||
var yamlParsingErrs []error
|
var yamlParsingErrs []error
|
||||||
for _, rm := range rms {
|
for _, rm := range rms {
|
||||||
@ -287,30 +208,27 @@ func parseCoderResourceReadmeFiles(resourceType string, rms []readme) (map[strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yamlValidationErrors := []error{}
|
var serialized []coderResourceReadme
|
||||||
for _, readme := range resources {
|
for _, r := range resources {
|
||||||
errs := validateCoderResourceReadme(readme)
|
serialized = append(serialized, r)
|
||||||
if len(errs) > 0 {
|
|
||||||
yamlValidationErrors = append(yamlValidationErrors, errs...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(yamlValidationErrors) != 0 {
|
slices.SortFunc(serialized, func(r1 coderResourceReadme, r2 coderResourceReadme) int {
|
||||||
return nil, validationPhaseError{
|
return strings.Compare(r1.filePath, r2.filePath)
|
||||||
phase: validationPhaseReadme,
|
})
|
||||||
errors: yamlValidationErrors,
|
return serialized, nil
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resources, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: Need to beef up this function by grabbing each image/video URL from
|
// Todo: Need to beef up this function by grabbing each image/video URL from
|
||||||
// the body's AST.
|
// the body's AST.
|
||||||
func validateCoderResourceRelativeURLs(_ map[string]coderResourceReadme) error {
|
func validateCoderResourceRelativeURLs(_ []coderResourceReadme) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
||||||
|
if !slices.Contains(supportedResourceTypes, resourceType) {
|
||||||
|
return nil, xerrors.Errorf("cannot process unknown resource type %q", resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
registryFiles, err := os.ReadDir(rootRegistryPath)
|
registryFiles, err := os.ReadDir(rootRegistryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -359,27 +277,3 @@ func aggregateCoderResourceReadmeFiles(resourceType string) ([]readme, error) {
|
|||||||
}
|
}
|
||||||
return allReadmeFiles, nil
|
return allReadmeFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAllCoderResourceFilesOfType(resourceType string) error {
|
|
||||||
if !slices.Contains(supportedResourceTypes, resourceType) {
|
|
||||||
return xerrors.Errorf("resource type %q is not part of supported list [%s]", resourceType, strings.Join(supportedResourceTypes, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
|
|
||||||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info(context.Background(), "processed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
|
|
||||||
|
|
||||||
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "type", resourceType)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
119
cmd/readmevalidation/codertemplates.go
Normal file
119
cmd/readmevalidation/codertemplates.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateCoderTemplateReadmeBody(body string) []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
trimmed := strings.TrimSpace(body)
|
||||||
|
if baseErrs := validateReadmeBody(trimmed); len(baseErrs) != 0 {
|
||||||
|
errs = append(errs, baseErrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextLine string
|
||||||
|
foundParagraph := false
|
||||||
|
isInsideCodeBlock := false
|
||||||
|
lineNum := 0
|
||||||
|
|
||||||
|
lineScanner := bufio.NewScanner(strings.NewReader(trimmed))
|
||||||
|
for lineScanner.Scan() {
|
||||||
|
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.
|
||||||
|
if lineNum == 1 {
|
||||||
|
if !strings.HasPrefix(nextLine, "# ") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(nextLine, "```") {
|
||||||
|
isInsideCodeBlock = !isInsideCodeBlock
|
||||||
|
if strings.HasPrefix(nextLine, "```hcl") {
|
||||||
|
errs = append(errs, xerrors.New("all .hcl language references must be converted to .tf"))
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
trimmedLine := strings.TrimSpace(nextLine)
|
||||||
|
isParagraph := trimmedLine != "" && !strings.HasPrefix(trimmedLine, "![") && !strings.HasPrefix(trimmedLine, "<")
|
||||||
|
foundParagraph = foundParagraph || isParagraph
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundParagraph {
|
||||||
|
errs = append(errs, xerrors.New("did not find paragraph within h1 section"))
|
||||||
|
}
|
||||||
|
if isInsideCodeBlock {
|
||||||
|
errs = append(errs, xerrors.New("code blocks inside h1 section do not all terminate before end of file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateCoderTemplateReadme(rm coderResourceReadme) []error {
|
||||||
|
var errs []error
|
||||||
|
for _, err := range validateCoderTemplateReadmeBody(rm.body) {
|
||||||
|
errs = append(errs, addFilePathToError(rm.filePath, err))
|
||||||
|
}
|
||||||
|
if fmErrs := validateCoderResourceFrontmatter("templates", rm.filePath, rm.frontmatter); len(fmErrs) != 0 {
|
||||||
|
errs = append(errs, fmErrs...)
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllCoderTemplateReadmes(resources []coderResourceReadme) error {
|
||||||
|
var yamlValidationErrors []error
|
||||||
|
for _, readme := range resources {
|
||||||
|
errs := validateCoderTemplateReadme(readme)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
yamlValidationErrors = append(yamlValidationErrors, errs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(yamlValidationErrors) != 0 {
|
||||||
|
return validationPhaseError{
|
||||||
|
phase: validationPhaseReadme,
|
||||||
|
errors: yamlValidationErrors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAllCoderTemplates() error {
|
||||||
|
const resourceType = "templates"
|
||||||
|
allReadmeFiles, err := aggregateCoderResourceReadmeFiles(resourceType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(context.Background(), "processing template README files", "resource_type", resourceType, "num_files", len(allReadmeFiles))
|
||||||
|
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = validateAllCoderTemplateReadmes(resources)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info(context.Background(), "processed README files as valid Coder resources", "resource_type", resourceType, "num_files", len(resources))
|
||||||
|
|
||||||
|
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info(context.Background(), "all relative URLs for READMEs are valid", "resource_type", resourceType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -79,7 +79,7 @@ func validateContributorSupportEmail(email *string) []error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := []error{}
|
var errs []error
|
||||||
|
|
||||||
username, server, ok := strings.Cut(*email, "@")
|
username, server, ok := strings.Cut(*email, "@")
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -140,7 +140,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
|||||||
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
|
return []error{xerrors.New("avatar URL must be omitted or non-empty string")}
|
||||||
}
|
}
|
||||||
|
|
||||||
errs := []error{}
|
var errs []error
|
||||||
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL.
|
// Have to use .Parse instead of .ParseRequestURI because this is the one field that's allowed to be a relative URL.
|
||||||
if _, err := url.Parse(*avatarURL); err != nil {
|
if _, err := url.Parse(*avatarURL); err != nil {
|
||||||
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
errs = append(errs, xerrors.Errorf("URL %q is not a valid relative or absolute URL", *avatarURL))
|
||||||
@ -166,7 +166,7 @@ func validateContributorAvatarURL(avatarURL *string) []error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateContributorReadme(rm contributorProfileReadme) []error {
|
func validateContributorReadme(rm contributorProfileReadme) []error {
|
||||||
allErrs := []error{}
|
var allErrs []error
|
||||||
|
|
||||||
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
|
if err := validateContributorDisplayName(rm.frontmatter.DisplayName); err != nil {
|
||||||
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
allErrs = append(allErrs, addFilePathToError(rm.filePath, err))
|
||||||
@ -202,7 +202,7 @@ func parseContributorProfile(rm readme) (contributorProfileReadme, []error) {
|
|||||||
|
|
||||||
keyErrs := validateFrontmatterYamlKeys(fm, supportedContributorProfileStructKeys)
|
keyErrs := validateFrontmatterYamlKeys(fm, supportedContributorProfileStructKeys)
|
||||||
if len(keyErrs) != 0 {
|
if len(keyErrs) != 0 {
|
||||||
remapped := []error{}
|
var remapped []error
|
||||||
for _, e := range keyErrs {
|
for _, e := range keyErrs {
|
||||||
remapped = append(remapped, addFilePathToError(rm.filePath, e))
|
remapped = append(remapped, addFilePathToError(rm.filePath, e))
|
||||||
}
|
}
|
||||||
@ -223,7 +223,7 @@ func parseContributorProfile(rm readme) (contributorProfileReadme, []error) {
|
|||||||
|
|
||||||
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
|
func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfileReadme, error) {
|
||||||
profilesByNamespace := map[string]contributorProfileReadme{}
|
profilesByNamespace := map[string]contributorProfileReadme{}
|
||||||
yamlParsingErrors := []error{}
|
var yamlParsingErrors []error
|
||||||
for _, rm := range readmeEntries {
|
for _, rm := range readmeEntries {
|
||||||
p, errs := parseContributorProfile(rm)
|
p, errs := parseContributorProfile(rm)
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
@ -244,7 +244,7 @@ func parseContributorFiles(readmeEntries []readme) (map[string]contributorProfil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
yamlValidationErrors := []error{}
|
var yamlValidationErrors []error
|
||||||
for _, p := range profilesByNamespace {
|
for _, p := range profilesByNamespace {
|
||||||
if errors := validateContributorReadme(p); len(errors) > 0 {
|
if errors := validateContributorReadme(p); len(errors) > 0 {
|
||||||
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
yamlValidationErrors = append(yamlValidationErrors, errors...)
|
||||||
@ -267,8 +267,8 @@ func aggregateContributorReadmeFiles() ([]readme, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
allReadmeFiles := []readme{}
|
var allReadmeFiles []readme
|
||||||
errs := []error{}
|
var errs []error
|
||||||
dirPath := ""
|
dirPath := ""
|
||||||
for _, e := range dirEntries {
|
for _, e := range dirEntries {
|
||||||
if !e.IsDir() {
|
if !e.IsDir() {
|
||||||
|
|||||||
@ -31,7 +31,11 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
err = validateAllCoderResourceFilesOfType("modules")
|
err = validateAllCoderModules()
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
err = validateAllCoderTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ tags: [vm, linux, gcp, devcontainer]
|
|||||||
|
|
||||||
# Remote Development in a Devcontainer on Google Compute Engine
|
# Remote Development in a Devcontainer on Google Compute Engine
|
||||||
|
|
||||||
|
Provision a Devcontainer on Google Compute Engine instances as Coder workspaces
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|||||||
@ -8,6 +8,8 @@ tags: [vm, linux, gcp]
|
|||||||
|
|
||||||
# Remote Development on Google Compute Engine (Linux)
|
# Remote Development on Google Compute Engine (Linux)
|
||||||
|
|
||||||
|
Provision Google Compute Engine instances as Coder workspaces
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|||||||
@ -8,6 +8,8 @@ tags: [vm-container, linux, gcp]
|
|||||||
|
|
||||||
# Remote Development on Google Compute Engine (VM Container)
|
# Remote Development on Google Compute Engine (VM Container)
|
||||||
|
|
||||||
|
Provision Google Compute Engine instances as Coder workspaces.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|||||||
@ -8,6 +8,8 @@ tags: [vm, windows, gcp]
|
|||||||
|
|
||||||
# Remote Development on Google Compute Engine (Windows)
|
# Remote Development on Google Compute Engine (Windows)
|
||||||
|
|
||||||
|
Provision Google Compute Engine instances as Coder workspaces
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|||||||
@ -8,6 +8,8 @@ tags: [kubernetes, containers, docker-in-docker]
|
|||||||
|
|
||||||
# envbox
|
# envbox
|
||||||
|
|
||||||
|
Provision envbox pods as Coder workspaces
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
`envbox` is an image that enables creating non-privileged containers capable of running system-level software (e.g. `dockerd`, `systemd`, etc) in Kubernetes.
|
`envbox` is an image that enables creating non-privileged containers capable of running system-level software (e.g. `dockerd`, `systemd`, etc) in Kubernetes.
|
||||||
|
|||||||
@ -1,27 +1,29 @@
|
|||||||
---
|
---
|
||||||
display_name: "Claude Code AI Agent Template"
|
display_name: "Claude Code AI Agent Template"
|
||||||
description: The goal is to try the experimental ai agent integration with Claude CodeAI agent
|
description: An experimental AI agent integration with Claude CodeAI agent
|
||||||
icon: "../../../../.icons/claude.svg"
|
icon: "../../../../.icons/claude.svg"
|
||||||
verified: false
|
verified: false
|
||||||
tags: ["ai", "docker", "container", "claude", "agent", "tasks"]
|
tags: ["ai", "docker", "container", "claude", "agent", "tasks"]
|
||||||
---
|
---
|
||||||
|
|
||||||
# ai agent template for a workspace in a container on a Docker host
|
# AI agent template for a workspace in a container on a Docker host
|
||||||
|
|
||||||
### Docker image
|
An experimental AI agent integration with Claude CodeAI agent
|
||||||
|
|
||||||
|
## Docker image
|
||||||
|
|
||||||
1. Based on Coder-managed image `codercom/example-universal:ubuntu`
|
1. Based on Coder-managed image `codercom/example-universal:ubuntu`
|
||||||
|
|
||||||
[Image on DockerHub](https://hub.docker.com/r/codercom/example-universal)
|
[Image on DockerHub](https://hub.docker.com/r/codercom/example-universal)
|
||||||
|
|
||||||
### Apps included
|
## Apps included
|
||||||
|
|
||||||
1. A web-based terminal
|
1. A web-based terminal
|
||||||
1. code-server Web IDE
|
1. code-server Web IDE
|
||||||
1. A [sample app](https://github.com/gothinkster/realworld) to test the environment
|
1. A [sample app](https://github.com/gothinkster/realworld) to test the environment
|
||||||
1. [Claude Code AI agent](https://www.anthropic.com/claude-code) to assist with development tasks
|
1. [Claude Code AI agent](https://www.anthropic.com/claude-code) to assist with development tasks
|
||||||
|
|
||||||
### Resources
|
## Resources
|
||||||
|
|
||||||
[Coder docs on AI agents and tasks](https://coder.com/docs/ai-coder/tasks)
|
[Coder docs on AI agents and tasks](https://coder.com/docs/ai-coder/tasks)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user