 3373f7b462
			
		
	
	3373f7b462
	
	
		
			
	
		
	
	**Problem**: The standardized label set was missing the `chorus-entrypoint` label, which is present in CHORUS repository and required for triggering council formation for project kickoffs. **Changes**: - Added `chorus-entrypoint` label (#ff6b6b) to `EnsureRequiredLabels()` in `internal/gitea/client.go` - Now creates 9 standard labels (was 8): 1. bug 2. bzzz-task 3. chorus-entrypoint (NEW) 4. duplicate 5. enhancement 6. help wanted 7. invalid 8. question 9. wontfix **Testing**: - Rebuilt and deployed WHOOSH with updated label configuration - Synced labels to all 5 monitored repositories (whoosh-ui, SequentialThinkingForCHORUS, TEST, WHOOSH, CHORUS) - Verified all repositories now have complete 9-label set **Impact**: All CHORUS ecosystem repositories now have consistent labeling matching the CHORUS repository standard, enabling proper council formation triggers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			523 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			523 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package gitea
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"os"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/chorus-services/whoosh/internal/config"
 | |
| 	"github.com/rs/zerolog/log"
 | |
| )
 | |
| 
 | |
| // Client represents a Gitea API client
 | |
| type Client struct {
 | |
| 	baseURL string
 | |
| 	token   string
 | |
| 	client  *http.Client
 | |
| 	config  config.GITEAConfig
 | |
| }
 | |
| 
 | |
| // Issue represents a Gitea issue
 | |
| type Issue struct {
 | |
| 	ID          int64           `json:"id"`
 | |
| 	Number      int64           `json:"number"`
 | |
| 	Title       string          `json:"title"`
 | |
| 	Body        string          `json:"body"`
 | |
| 	State       string          `json:"state"`
 | |
| 	Labels      []Label         `json:"labels"`
 | |
| 	Assignees   []User          `json:"assignees"`
 | |
| 	CreatedAt   time.Time       `json:"created_at"`
 | |
| 	UpdatedAt   time.Time       `json:"updated_at"`
 | |
| 	ClosedAt    *time.Time      `json:"closed_at"`
 | |
| 	HTMLURL     string          `json:"html_url"`
 | |
| 	User        User            `json:"user"`
 | |
| 	Repository  IssueRepository `json:"repository,omitempty"`
 | |
| }
 | |
| 
 | |
| // Label represents a Gitea issue label
 | |
| type Label struct {
 | |
| 	ID          int64  `json:"id"`
 | |
| 	Name        string `json:"name"`
 | |
| 	Color       string `json:"color"`
 | |
| 	Description string `json:"description"`
 | |
| }
 | |
| 
 | |
| // User represents a Gitea user
 | |
| type User struct {
 | |
| 	ID       int64  `json:"id"`
 | |
| 	Login    string `json:"login"`
 | |
| 	FullName string `json:"full_name"`
 | |
| 	Email    string `json:"email"`
 | |
| 	AvatarURL string `json:"avatar_url"`
 | |
| }
 | |
| 
 | |
| // Repository represents a Gitea repository
 | |
| type Repository struct {
 | |
| 	ID          int64  `json:"id"`
 | |
| 	Name        string `json:"name"`
 | |
| 	FullName    string `json:"full_name"`
 | |
| 	Owner       User   `json:"owner"`
 | |
| 	Description string `json:"description"`
 | |
| 	Private     bool   `json:"private"`
 | |
| 	HTMLURL     string `json:"html_url"`
 | |
| 	CloneURL    string `json:"clone_url"`
 | |
| 	SSHURL      string `json:"ssh_url"`
 | |
| 	Language    string `json:"language"`
 | |
| }
 | |
| 
 | |
| // IssueRepository represents the simplified repository info in issue responses
 | |
| type IssueRepository struct {
 | |
| 	ID       int64  `json:"id"`
 | |
| 	Name     string `json:"name"`
 | |
| 	FullName string `json:"full_name"`
 | |
| 	Owner    string `json:"owner"` // Note: This is a string, not a User object
 | |
| }
 | |
| 
 | |
| // NewClient creates a new Gitea API client
 | |
| func NewClient(cfg config.GITEAConfig) *Client {
 | |
| 	token := cfg.Token
 | |
| 	// Load token from file if TokenFile is specified and Token is empty
 | |
| 	if token == "" && cfg.TokenFile != "" {
 | |
| 		if fileToken, err := os.ReadFile(cfg.TokenFile); err == nil {
 | |
| 			token = strings.TrimSpace(string(fileToken))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return &Client{
 | |
| 		baseURL: cfg.BaseURL,
 | |
| 		token:   token,
 | |
| 		config:  cfg,
 | |
| 		client: &http.Client{
 | |
| 			Timeout: 30 * time.Second,
 | |
| 		},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // makeRequest makes an authenticated request to the Gitea API with retry logic
 | |
| func (c *Client) makeRequest(ctx context.Context, method, endpoint string) (*http.Response, error) {
 | |
| 	url := fmt.Sprintf("%s/api/v1%s", c.baseURL, endpoint)
 | |
| 	
 | |
| 	if c.config.DebugURLs {
 | |
| 		log.Debug().
 | |
| 			Str("method", method).
 | |
| 			Str("url", url).
 | |
| 			Msg("Making Gitea API request")
 | |
| 	}
 | |
| 	
 | |
| 	var lastErr error
 | |
| 	for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
 | |
| 		if attempt > 0 {
 | |
| 			select {
 | |
| 			case <-ctx.Done():
 | |
| 				return nil, ctx.Err()
 | |
| 			case <-time.After(c.config.RetryDelay):
 | |
| 				// Continue with retry
 | |
| 			}
 | |
| 			
 | |
| 			if c.config.DebugURLs {
 | |
| 				log.Debug().
 | |
| 					Int("attempt", attempt).
 | |
| 					Str("url", url).
 | |
| 					Msg("Retrying Gitea API request")
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		req, err := http.NewRequestWithContext(ctx, method, url, nil)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("failed to create request: %w", err)
 | |
| 		}
 | |
| 		
 | |
| 		if c.token != "" {
 | |
| 			req.Header.Set("Authorization", "token "+c.token)
 | |
| 		}
 | |
| 		req.Header.Set("Content-Type", "application/json")
 | |
| 		req.Header.Set("Accept", "application/json")
 | |
| 		
 | |
| 		resp, err := c.client.Do(req)
 | |
| 		if err != nil {
 | |
| 			lastErr = fmt.Errorf("failed to make request: %w", err)
 | |
| 			log.Warn().
 | |
| 				Err(err).
 | |
| 				Str("url", url).
 | |
| 				Int("attempt", attempt).
 | |
| 				Msg("Gitea API request failed")
 | |
| 			continue
 | |
| 		}
 | |
| 		
 | |
| 		if resp.StatusCode >= 400 {
 | |
| 			defer resp.Body.Close()
 | |
| 			lastErr = fmt.Errorf("API request failed with status %d", resp.StatusCode)
 | |
| 			
 | |
| 			// Only retry on specific status codes (5xx errors, rate limiting)
 | |
| 			if resp.StatusCode >= 500 || resp.StatusCode == 429 {
 | |
| 				log.Warn().
 | |
| 					Int("status_code", resp.StatusCode).
 | |
| 					Str("url", url).
 | |
| 					Int("attempt", attempt).
 | |
| 					Msg("Retryable Gitea API error")
 | |
| 				continue
 | |
| 			}
 | |
| 			
 | |
| 			// Don't retry on 4xx errors (client errors)
 | |
| 			return nil, lastErr
 | |
| 		}
 | |
| 		
 | |
| 		// Success
 | |
| 		return resp, nil
 | |
| 	}
 | |
| 	
 | |
| 	return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
 | |
| }
 | |
| 
 | |
| // GetRepository retrieves repository information
 | |
| func (c *Client) GetRepository(ctx context.Context, owner, repo string) (*Repository, error) {
 | |
| 	endpoint := fmt.Sprintf("/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repo))
 | |
| 	
 | |
| 	resp, err := c.makeRequest(ctx, "GET", endpoint)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get repository: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	var repository Repository
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode repository: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return &repository, nil
 | |
| }
 | |
| 
 | |
| // GetIssues retrieves issues from a repository with hardening features
 | |
| func (c *Client) GetIssues(ctx context.Context, owner, repo string, opts IssueListOptions) ([]Issue, error) {
 | |
| 	endpoint := fmt.Sprintf("/repos/%s/%s/issues", url.PathEscape(owner), url.PathEscape(repo))
 | |
| 	
 | |
| 	// Add query parameters
 | |
| 	params := url.Values{}
 | |
| 	if opts.State != "" {
 | |
| 		params.Set("state", opts.State)
 | |
| 	}
 | |
| 	
 | |
| 	// EAGER_FILTER: Apply label pre-filtering at the API level for efficiency
 | |
| 	if c.config.EagerFilter && opts.Labels != "" {
 | |
| 		params.Set("labels", opts.Labels)
 | |
| 		if c.config.DebugURLs {
 | |
| 			log.Debug().
 | |
| 				Str("labels", opts.Labels).
 | |
| 				Bool("eager_filter", true).
 | |
| 				Msg("Applying eager label filtering")
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	if opts.Page > 0 {
 | |
| 		params.Set("page", strconv.Itoa(opts.Page))
 | |
| 	}
 | |
| 	if opts.Limit > 0 {
 | |
| 		params.Set("limit", strconv.Itoa(opts.Limit))
 | |
| 	}
 | |
| 	
 | |
| 	// FULL_RESCAN: Optionally ignore since parameter for complete rescan
 | |
| 	if !c.config.FullRescan && !opts.Since.IsZero() {
 | |
| 		params.Set("since", opts.Since.Format(time.RFC3339))
 | |
| 		if c.config.DebugURLs {
 | |
| 			log.Debug().
 | |
| 				Time("since", opts.Since).
 | |
| 				Msg("Using since parameter for incremental fetch")
 | |
| 		}
 | |
| 	} else if c.config.FullRescan {
 | |
| 		if c.config.DebugURLs {
 | |
| 			log.Debug().
 | |
| 				Bool("full_rescan", true).
 | |
| 				Msg("Performing full rescan (ignoring since parameter)")
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	if len(params) > 0 {
 | |
| 		endpoint += "?" + params.Encode()
 | |
| 	}
 | |
| 	
 | |
| 	resp, err := c.makeRequest(ctx, "GET", endpoint)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get issues: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	var issues []Issue
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode issues: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Apply in-code filtering when EAGER_FILTER is disabled
 | |
| 	if !c.config.EagerFilter && opts.Labels != "" {
 | |
| 		issues = c.filterIssuesByLabels(issues, opts.Labels)
 | |
| 		if c.config.DebugURLs {
 | |
| 			log.Debug().
 | |
| 				Str("labels", opts.Labels).
 | |
| 				Bool("eager_filter", false).
 | |
| 				Int("filtered_count", len(issues)).
 | |
| 				Msg("Applied in-code label filtering")
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	// Set repository information on each issue for context
 | |
| 	for i := range issues {
 | |
| 		issues[i].Repository = IssueRepository{
 | |
| 			Name:     repo,
 | |
| 			FullName: fmt.Sprintf("%s/%s", owner, repo),
 | |
| 			Owner:    owner, // Now a string instead of User object
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	if c.config.DebugURLs {
 | |
| 		log.Debug().
 | |
| 			Str("owner", owner).
 | |
| 			Str("repo", repo).
 | |
| 			Int("issue_count", len(issues)).
 | |
| 			Msg("Gitea issues fetched successfully")
 | |
| 	}
 | |
| 	
 | |
| 	return issues, nil
 | |
| }
 | |
| 
 | |
| // filterIssuesByLabels filters issues by label names (in-code filtering when eager filter is disabled)
 | |
| func (c *Client) filterIssuesByLabels(issues []Issue, labelFilter string) []Issue {
 | |
| 	if labelFilter == "" {
 | |
| 		return issues
 | |
| 	}
 | |
| 	
 | |
| 	// Parse comma-separated label names
 | |
| 	requiredLabels := strings.Split(labelFilter, ",")
 | |
| 	for i, label := range requiredLabels {
 | |
| 		requiredLabels[i] = strings.TrimSpace(label)
 | |
| 	}
 | |
| 	
 | |
| 	var filtered []Issue
 | |
| 	for _, issue := range issues {
 | |
| 		hasRequiredLabels := true
 | |
| 		
 | |
| 		for _, requiredLabel := range requiredLabels {
 | |
| 			found := false
 | |
| 			for _, issueLabel := range issue.Labels {
 | |
| 				if issueLabel.Name == requiredLabel {
 | |
| 					found = true
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			if !found {
 | |
| 				hasRequiredLabels = false
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		
 | |
| 		if hasRequiredLabels {
 | |
| 			filtered = append(filtered, issue)
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return filtered
 | |
| }
 | |
| 
 | |
| // GetIssue retrieves a specific issue
 | |
| func (c *Client) GetIssue(ctx context.Context, owner, repo string, issueNumber int64) (*Issue, error) {
 | |
| 	endpoint := fmt.Sprintf("/repos/%s/%s/issues/%d", url.PathEscape(owner), url.PathEscape(repo), issueNumber)
 | |
| 	
 | |
| 	resp, err := c.makeRequest(ctx, "GET", endpoint)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get issue: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	var issue Issue
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode issue: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	// Set repository information
 | |
| 	issue.Repository = IssueRepository{
 | |
| 		Name:     repo,
 | |
| 		FullName: fmt.Sprintf("%s/%s", owner, repo),
 | |
| 		Owner:    owner, // Now a string instead of User object
 | |
| 	}
 | |
| 	
 | |
| 	return &issue, nil
 | |
| }
 | |
| 
 | |
| // IssueListOptions contains options for listing issues
 | |
| type IssueListOptions struct {
 | |
| 	State  string    // "open", "closed", "all"
 | |
| 	Labels string    // Comma-separated list of label names
 | |
| 	Page   int       // Page number (1-based)
 | |
| 	Limit  int       // Number of items per page (default: 20, max: 100)
 | |
| 	Since  time.Time // Only show issues updated after this time
 | |
| }
 | |
| 
 | |
| // TestConnection tests the connection to Gitea API
 | |
| func (c *Client) TestConnection(ctx context.Context) error {
 | |
| 	resp, err := c.makeRequest(ctx, "GET", "/user")
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("connection test failed: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // WebhookPayload represents a Gitea webhook payload
 | |
| type WebhookPayload struct {
 | |
| 	Action     string     `json:"action"`
 | |
| 	Number     int64      `json:"number,omitempty"`
 | |
| 	Issue      *Issue     `json:"issue,omitempty"`
 | |
| 	Repository Repository `json:"repository"`
 | |
| 	Sender     User       `json:"sender"`
 | |
| }
 | |
| 
 | |
| // CreateLabelRequest represents the request to create a new label
 | |
| type CreateLabelRequest struct {
 | |
| 	Name        string `json:"name"`
 | |
| 	Color       string `json:"color"`
 | |
| 	Description string `json:"description"`
 | |
| }
 | |
| 
 | |
| // CreateLabel creates a new label in a repository
 | |
| func (c *Client) CreateLabel(ctx context.Context, owner, repo string, label CreateLabelRequest) (*Label, error) {
 | |
| 	endpoint := fmt.Sprintf("/repos/%s/%s/labels", url.PathEscape(owner), url.PathEscape(repo))
 | |
| 	
 | |
| 	jsonData, err := json.Marshal(label)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to marshal label data: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/api/v1%s", c.baseURL, endpoint), strings.NewReader(string(jsonData)))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to create request: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	if c.token != "" {
 | |
| 		req.Header.Set("Authorization", "token "+c.token)
 | |
| 	}
 | |
| 	req.Header.Set("Content-Type", "application/json")
 | |
| 	req.Header.Set("Accept", "application/json")
 | |
| 	
 | |
| 	resp, err := c.client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to make request: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	if resp.StatusCode >= 400 {
 | |
| 		return nil, fmt.Errorf("API request failed with status %d", resp.StatusCode)
 | |
| 	}
 | |
| 	
 | |
| 	var createdLabel Label
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&createdLabel); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode label: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return &createdLabel, nil
 | |
| }
 | |
| 
 | |
| // GetLabels retrieves all labels from a repository
 | |
| func (c *Client) GetLabels(ctx context.Context, owner, repo string) ([]Label, error) {
 | |
| 	endpoint := fmt.Sprintf("/repos/%s/%s/labels", url.PathEscape(owner), url.PathEscape(repo))
 | |
| 	
 | |
| 	resp, err := c.makeRequest(ctx, "GET", endpoint)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get labels: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	
 | |
| 	var labels []Label
 | |
| 	if err := json.NewDecoder(resp.Body).Decode(&labels); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to decode labels: %w", err)
 | |
| 	}
 | |
| 	
 | |
| 	return labels, nil
 | |
| }
 | |
| 
 | |
| // @goal: WHOOSH-LABELS-004, WSH-CONSISTENCY - Ecosystem standardization
 | |
| // WHY: Ensures all CHORUS ecosystem repositories have consistent GitHub-standard labels
 | |
| // EnsureRequiredLabels ensures that required labels exist in the repository
 | |
| func (c *Client) EnsureRequiredLabels(ctx context.Context, owner, repo string) error {
 | |
| 	// @goal: WHOOSH-LABELS-004 - Standardized label set matching WHOOSH repository conventions
 | |
| 	// WHY: Provides consistent issue categorization across all CHORUS ecosystem repositories
 | |
| 	requiredLabels := []CreateLabelRequest{
 | |
| 		{
 | |
| 			Name:        "bug",
 | |
| 			Color:       "ee0701",
 | |
| 			Description: "Something is not working",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "bzzz-task",
 | |
| 			Color:       "5319e7", // @goal: WHOOSH-LABELS-004 - Corrected color to match ecosystem standard
 | |
| 			Description: "CHORUS task for auto ingestion.",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "chorus-entrypoint",
 | |
| 			Color:       "ff6b6b",
 | |
| 			Description: "Marks issues that trigger council formation for project kickoffs",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "duplicate",
 | |
| 			Color:       "cccccc",
 | |
| 			Description: "This issue or pull request already exists",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "enhancement",
 | |
| 			Color:       "84b6eb",
 | |
| 			Description: "New feature",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "help wanted",
 | |
| 			Color:       "128a0c",
 | |
| 			Description: "Need some help",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "invalid",
 | |
| 			Color:       "e6e6e6",
 | |
| 			Description: "Something is wrong",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "question",
 | |
| 			Color:       "cc317c",
 | |
| 			Description: "More information is needed",
 | |
| 		},
 | |
| 		{
 | |
| 			Name:        "wontfix",
 | |
| 			Color:       "ffffff",
 | |
| 			Description: "This won't be fixed",
 | |
| 		},
 | |
| 	}
 | |
| 	
 | |
| 	// @goal: WHOOSH-LABELS-004 - Check existing labels to avoid duplicates
 | |
| 	// WHY: Prevents API errors from attempting to create labels that already exist
 | |
| 	existingLabels, err := c.GetLabels(ctx, owner, repo)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get existing labels: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// @goal: WHOOSH-LABELS-004 - Build lookup map for efficient duplicate checking
 | |
| 	// WHY: O(1) lookup performance for label existence checking
 | |
| 	existingLabelNames := make(map[string]bool)
 | |
| 	for _, label := range existingLabels {
 | |
| 		existingLabelNames[label.Name] = true
 | |
| 	}
 | |
| 
 | |
| 	// @goal: WHOOSH-LABELS-004 - Create only missing standardized labels
 | |
| 	// WHY: Ensures all repositories have consistent labeling without overwriting existing labels
 | |
| 	for _, requiredLabel := range requiredLabels {
 | |
| 		if !existingLabelNames[requiredLabel.Name] {
 | |
| 			_, err := c.CreateLabel(ctx, owner, repo, requiredLabel)
 | |
| 			if err != nil {
 | |
| 				return fmt.Errorf("failed to create label %s: %w", requiredLabel.Name, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	
 | |
| 	return nil
 | |
| }
 |