Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
**/testdata/repository.git/objects/*/* ignore-lint=true
templates/node/*/package-lock.json ignore-lint=true
templates/typescript/*/package-lock.json ignore-lint=true
zz_filesystem_generated.go linguist-generated=true
pkg/docker/zz_close_guarding_client_generated.go linguist-generated=true
pkg/oci/testdata/test-links/* ignore-lint=true

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/pkg/functions/testdata/default_home/go
/pkg/functions/testdata/default_home/.config
/pkg/functions/testdata/default_home/.cache
/generate/templates.zip
/pkg/functions/testdata/migrations/*/.gitignore
/pkg/functions/testdata/default_home/Library

Expand Down
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ help:
build: $(BIN) ## (default) Build binary for current OS

.PHONY: $(BIN)
$(BIN): generate/zz_filesystem_generated.go
$(BIN): generate/templates.zip
env CGO_ENABLED=0 go build ./cmd/$(BIN)

.PHONY: test
test: generate/zz_filesystem_generated.go ## Run core unit tests
test: generate/templates.zip ## Run core unit tests
go test -race -cover -coverprofile=coverage.txt ./...

.PHONY: check
Expand Down Expand Up @@ -143,8 +143,8 @@ $(BIN_GOIMPORTS):
@echo "Installing goimports..."
@GOBIN=$(PWD)/bin go install golang.org/x/tools/cmd/goimports@latest

.PHONY: generate/zz_filesystem_generated.go
generate/zz_filesystem_generated.go: clean_templates
.PHONY: generate/templates.zip
generate/templates.zip: clean_templates
go generate pkg/functions/templates_embedded.go

.PHONY: clean_templates
Expand Down Expand Up @@ -178,6 +178,7 @@ clean: clean_templates ## Remove generated artifacts such as binaries and schema
rm -f $(BIN_GOLANGCI_LINT)
rm -f schema/func_yaml-schema.json
rm -f coverage.txt
rm -f generate/templates.zip

.PHONY: docs
docs:
Expand Down
6 changes: 6 additions & 0 deletions generate/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package generate

import _ "embed"

//go:embed templates.zip

Check failure on line 5 in generate/embed.go

View workflow job for this annotation

GitHub Actions / Precheck

pattern templates.zip: no matching files found (typecheck)

Check failure on line 5 in generate/embed.go

View workflow job for this annotation

GitHub Actions / analyze / Analyze CodeQL

pattern templates.zip: no matching files found

Check failure on line 5 in generate/embed.go

View workflow job for this annotation

GitHub Actions / style / Golang / Lint

pattern templates.zip: no matching files found (typecheck)
var TemplatesZip []byte
97 changes: 97 additions & 0 deletions generate/templates/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"bytes"
"crypto/sha256"
"fmt"
"io/fs"
"os"
"path/filepath"
)

// runCheck performs a 3-tier verification that generate/templates.zip is up-to-date.
// It is invoked by hack/verify-codegen.sh via:
//
// go run ./generate/templates check
func runCheck() error {
repoRoot, err := findRepoRoot()
if err != nil {
return err
}

zipPath := filepath.Join(repoRoot, "generate", "templates.zip")
templatesDir := filepath.Join(repoRoot, "templates")

var errs []error

// Check 1 — Existence
if _, err := os.Stat(zipPath); os.IsNotExist(err) {
return fmt.Errorf("generate/templates.zip not found. Run 'make generate/templates.zip' first")
}

// Check 2 — Staleness (fast mtime check)
zipInfo, err := os.Stat(zipPath)
if err != nil {
return fmt.Errorf("cannot stat generate/templates.zip: %w", err)
}
newerFound := ""
_ = filepath.Walk(templatesDir, func(path string, info fs.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
if info.ModTime().After(zipInfo.ModTime()) {
newerFound = path
return filepath.SkipAll
}
return nil
})
if newerFound != "" {
errs = append(errs, fmt.Errorf("generate/templates.zip is stale (file %s is newer). Run 'make generate/templates.zip'", newerFound))
}

// Check 3 — Content hash (definitive, catches branch-switch scenarios)
existingData, err := os.ReadFile(zipPath)
if err != nil {
return fmt.Errorf("cannot read templates.zip: %w", err)
}
existingHash := sha256.Sum256(existingData)

var buf bytes.Buffer
if err := writeZip(&buf, templatesDir); err != nil {
return fmt.Errorf("failed to regenerate zip for comparison: %w", err)
}
regeneratedHash := sha256.Sum256(buf.Bytes())

if existingHash != regeneratedHash {
errs = append(errs, fmt.Errorf("generate/templates.zip content mismatch. The regenerated zip differs from the existing one. Run 'make generate/templates.zip'"))
}

if len(errs) > 0 {
for _, e := range errs {
fmt.Fprintln(os.Stderr, "ERROR:", e)
}
return fmt.Errorf("generate/templates.zip is out of date")
}

fmt.Println("generate/templates.zip is up to date.")
return nil
}

// findRepoRoot walks up from the current directory to find the repo root
// (identified by the presence of go.mod).
func findRepoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", fmt.Errorf("could not find repo root (no go.mod found)")
}
dir = parent
}
}
166 changes: 166 additions & 0 deletions generate/templates/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package main

import (
"archive/zip"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"

ignore "github.com/sabhiram/go-gitignore"
)

// runGenerate writes a binary zip of the templates/ directory to generate/templates.zip.
func runGenerate() error {
repoRoot, err := findRepoRoot()
if err != nil {
return err
}
zipPath := filepath.Join(repoRoot, "generate", "templates.zip")
templatesDir := filepath.Join(repoRoot, "templates")

f, err := os.OpenFile(zipPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
return writeZip(f, templatesDir)
}

// writeZip creates a zip archive of the given templatesDir into w,
// respecting .gitignore files found within.
func writeZip(w io.Writer, templatesDir string) error {
zipWriter := zip.NewWriter(w)
buff := make([]byte, 4*1024)

// gitignoreCache caches compiled gitignore rules keyed by the directory containing .gitignore.
gitignoreCache := map[string]*ignore.GitIgnore{}

err := filepath.Walk(templatesDir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

name, err := filepath.Rel(templatesDir, path)
if err != nil {
return err
}
if name == "." {
return nil
}
name = filepath.ToSlash(name)

// Check if this path should be ignored by any .gitignore along the path.
if shouldIgnore(path, name, info.IsDir(), templatesDir, gitignoreCache) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}

// Load .gitignore from the current directory if it exists and not yet cached.
if info.IsDir() {
gitignorePath := filepath.Join(path, ".gitignore")
if _, statErr := os.Stat(gitignorePath); statErr == nil {
if _, cached := gitignoreCache[path]; !cached {
compiled, compileErr := ignore.CompileIgnoreFile(gitignorePath)
if compileErr == nil {
gitignoreCache[path] = compiled
}
}
}
}

if info.IsDir() {
name = name + "/"
}

header := &zip.FileHeader{
Name: name,
Method: zip.Deflate,
}

// Coercing permission to 755 for directories/executables and to 644 for non-executable files.
// This is needed to ensure reproducible builds on machines with different values of `umask`.
var mode fs.FileMode
switch {
case info.Mode()&fs.ModeSymlink != 0:
mode = 0777 | fs.ModeSymlink
case info.IsDir() || (info.Mode().Perm()&0111) != 0: // dir or executable
mode = 0755
case info.Mode()&fs.ModeType == 0: // regular file
mode = 0644
default:
return fmt.Errorf("unsupported file type: %s", info.Mode().String())
}
header.SetMode(mode)

zw, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}

switch {
case info.Mode()&fs.ModeSymlink != 0:
symlinkTarget, err := os.Readlink(path)
if err != nil {
return err
}
_, err = zw.Write([]byte(filepath.ToSlash(symlinkTarget)))
return err
case info.Mode()&fs.ModeType == 0: // regular file
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()

_, err = io.CopyBuffer(zw, f, buff)
return err
default:
return nil
}
})
zipWriter.Close()
return err
}

// shouldIgnore returns true if the given path matches any .gitignore rule from
// any parent directory up to (and including) the templates root.
// isDir controls whether a trailing slash is appended when matching, which is
// required for gitignore patterns like "target/" that only match directories.
func shouldIgnore(absPath, relFromTemplates string, isDir bool, templatesRoot string, cache map[string]*ignore.GitIgnore) bool {
// Walk up from the file's parent directory to the templates root,
// checking each directory's .gitignore rules.
dir := filepath.Dir(absPath)
for {
if gi, ok := cache[dir]; ok {
// Compute the path relative to the directory containing .gitignore.
rel, err := filepath.Rel(dir, absPath)
if err == nil {
rel = filepath.ToSlash(rel)
// Append trailing slash for directories so that patterns like
// "target/" (directory-only) are matched correctly.
if isDir {
rel = rel + "/"
}
if gi.MatchesPath(rel) {
return true
}
}
}

// Stop after processing the templates root directory.
if dir == templatesRoot || !strings.HasPrefix(dir, templatesRoot) {
break
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return false
}
Loading
Loading