package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "sync" "github.com/google/uuid" "github.com/joho/godotenv" ) type RepoType int const ( Unknown RepoType = iota GitHub GitLab BitBucket ) type RepoSetting struct { Addr string `json:"addr"` Dir string `json:"dir"` TagRegex string `json:"tagRegex"` Ignore string `json:"ignore"` } // RepoType と ドメインのマッピング var repoTypeMap = map[RepoType]string{ GitHub: "https://raw.githubusercontent.com", } func main() { godotenv.Load(".env") npmUser := os.Getenv("NPM_USER") npmPass := os.Getenv("NPM_PASS") npmRegistry := os.Getenv("NPM_REGISTRY") isDevMode := os.Getenv("DEV_MODE") == "true" jsonFiles, _ := filepath.Glob("./repos/*.json") token, err := getNpmAuthToken(npmRegistry, npmUser, npmPass) if err != nil { fmt.Printf("[Error] NPM Auth Failed. %s\n", err) return } var wg sync.WaitGroup for _, jsonFile := range jsonFiles { root, err := os.Getwd() if err != nil { fmt.Printf("[Error] Root Directory Get Failed. %s\n", err) continue } fileContent, err := os.ReadFile(jsonFile) if err != nil { fmt.Printf("[Error] File Read Failed. %s\n", err) continue } var repoSetting RepoSetting if err := json.Unmarshal(fileContent, &repoSetting); err != nil { fmt.Printf("[Error] JSON Parse Failed. %s\n", err) continue } wg.Add(1) go processRepository(root, repoSetting, token, npmRegistry, &wg, isDevMode) } wg.Wait() } func processRepository(workRoot string, repoSetting RepoSetting, npmToken, npmRegistry string, wg *sync.WaitGroup, devMode bool) { defer wg.Done() // 変数の準備 uuid := uuid.New().String() addr := repoSetting.Addr dir := repoSetting.Dir tagRegex := repoSetting.TagRegex ignore := repoSetting.Ignore == "true" repositoryName := strings.TrimSuffix(filepath.Base(addr), filepath.Ext(addr)) repositoryAuthor := strings.Split(addr, "/")[len(strings.Split(addr, "/"))-2] repositoryDirName := fmt.Sprintf("_target_%s-%s", repositoryName, uuid) repositoryRoot := filepath.Join(workRoot, repositoryDirName) packageRoot := filepath.Join(repositoryRoot, dir) var npmVersions map[string][]string = make(map[string][]string) var repositoryType RepoType = Unknown if strings.HasPrefix(addr, "https://github.com/") { repositoryType = GitHub } if tagRegex == "" { tagRegex = "^[0-9]+\\.[0-9]+\\.[0-9]+$" } if dir == "" { dir = "." } if ignore { fmt.Printf("[Info] Ignored. %s:%s\n", repositoryName, dir) return } // Tagの確認 remoteTags, err := getRemoteTags(addr) if err != nil { fmt.Printf("[Error] Invalid Repository. %s\n", addr) return } var validTags []string // 有効なTagのみを抽出 for _, tag := range remoteTags { matched, _ := regexp.MatchString(tagRegex, tag) if !matched { fmt.Printf("[Warn] Invalid Tag. %s:%s@%s\n", repositoryName, dir, tag) } else { validTags = append(validTags, tag) } } var unpublishedTags []string tagChan := make(chan string) // 非同期に未アップロードのタグを抽出 for _, tag := range validTags { go func(tag string) { repositoryJSON, err := getPackageJson(repositoryType, repositoryAuthor, repositoryName, tag, dir) if err != nil { fmt.Printf("[Warn] Invalid Path. %s:%s@%s\n", repositoryName, dir, tag) tagChan <- "" return } packageName := repositoryJSON["name"].(string) packageVersion := regexp.MustCompile(`[0-9]+\.[0-9]+\.[0-9]+`).FindString(tag) if npmVersions[packageName] == nil { npmVersions[packageName], err = getNpmTags("https://"+npmRegistry, packageName) if err != nil { fmt.Printf("[Warn] Invalid Repository. %s\n", err) // 未アップロードなだけなのでエラーとしない } } // サーバーに存在するか確認 existOnServer := false for _, npmVersion := range npmVersions[packageName] { if npmVersion == packageVersion { existOnServer = true break } } if existOnServer { fmt.Printf("[Info] Already Published. %s:%s@%s\n", repositoryName, dir, tag) tagChan <- "" } else { tagChan <- tag } }(tag) } // チャネルから結果を受け取る for range validTags { tag := <-tagChan if tag != "" { unpublishedTags = append(unpublishedTags, tag) } } unpublishedTags = sortTags(unpublishedTags) // 未アップロードのパッケージを処理 for _, tag := range unpublishedTags { if err := os.Chdir(workRoot); err != nil { fmt.Printf("[Error] Work Directory Change Failed. %s\n", err) continue } // repositoryDirName ディレクトリが無ければtagタグをClone, あればtagタグをチェックアウト if _, err := os.Stat(repositoryRoot); os.IsNotExist(err) { if err := exec.Command("git", "clone", "-b", tag, addr, repositoryDirName).Run(); err != nil { fmt.Printf("[Error] clone failed. %s\n", err) continue } if err := os.Chdir(repositoryRoot); err != nil { fmt.Printf("[Error] directory change failed. %s\n", err) continue } } else { if err := os.Chdir(repositoryRoot); err != nil { fmt.Printf("[Error] directory change failed. %s\n", err) continue } if err := exec.Command("git", "checkout", tag).Run(); err != nil { fmt.Printf("[Error] checkout failed. %s\n", err) continue } } if err := exec.Command("git", "submodule", "update", "--init", "--recursive").Run(); err != nil { fmt.Printf("[Error] submodule update failed. %s\n", err) continue } if err := exec.Command("git", "lfs", "pull").Run(); err != nil { fmt.Printf("[Error] lfs pull failed. %s\n", err) continue } if err := os.Chdir(packageRoot); err != nil { fmt.Printf("[Error] directory change failed. %s\n", err) continue } // パッケージのアップロード if err := exec.Command("npm", "set", fmt.Sprintf("//%s/:_authToken", npmRegistry), npmToken).Run(); err != nil { fmt.Printf("[Error] npm auth failed. %s\n", err) continue } if !devMode { if err := exec.Command("npm", "publish", "--registry", fmt.Sprintf("https://%s/", npmRegistry)).Run(); err != nil { fmt.Printf("%s:%s@%s failed to publish. %s\n", repositoryName, dir, tag, err) } else { fmt.Printf("%s:%s@%s published.\n", repositoryName, dir, tag) } } else { fmt.Printf("%s:%s@%s dummy published.\n", repositoryName, dir, tag) } } // 作業ディレクトリの削除 if err := os.Chdir(workRoot); err != nil { fmt.Printf("[Error] Work Directory Change Failed. %s\n", err) } if err := os.RemoveAll(repositoryRoot); err != nil { fmt.Printf("[Error] Work Directory Remove Failed. %s\n", err) } } // タグをソートする関数です。 func sortTags(tags []string) []string { sort.Slice(tags, func(i, j int) bool { versionI := strings.Split(tags[i], ".") versionJ := strings.Split(tags[j], ".") for k := 0; k < len(versionI) && k < len(versionJ); k++ { numI, _ := strconv.Atoi(versionI[k]) numJ, _ := strconv.Atoi(versionJ[k]) if numI != numJ { return numI < numJ } } return len(versionI) < len(versionJ) }) return tags } // 指定されたリポジトリのタグ一覧を取得する関数です。 func getRemoteTags(repoAddr string) ([]string, error) { // git ls-remote --tags コマンドを実行 cmd := exec.Command("git", "ls-remote", "--tags", repoAddr) out, err := cmd.Output() if err != nil { return nil, err } // 出力をパースしてタグを抽出 lines := strings.Split(string(out), "\n") var tags []string for _, line := range lines { if line == "" { continue } parts := strings.Split(line, "\t") if len(parts) < 2 { continue } tag := strings.Replace(parts[1], "refs/tags/", "", 1) tags = append(tags, tag) } return tags, nil } // 指定された npm レジストリとパッケージ名を使用して、npm パッケージのタグ一覧を取得する関数です。 func getNpmTags(npmRepo string, packageName string) ([]string, error) { var url = fmt.Sprintf("%s/%s", npmRepo, packageName) // HTTP GET リクエストを送信 resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() // レスポンスのボディを読み取る body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } // JSONをパースしてmapに変換 var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } if result["versions"] == nil { return nil, fmt.Errorf("versions not found") } // バージョン情報一覧を取得 var versions []string for version := range result["versions"].(map[string]interface{}) { versions = append(versions, version) } return versions, nil } // 指定されたリポジトリの package.json を取得する関数です。 func getPackageJson(repoType RepoType, author string, repository string, tag string, dir string) (map[string]interface{}, error) { var url = fmt.Sprintf("%s/%s/%s/%s/%s/package.json", repoTypeMap[repoType], author, repository, tag, dir) // HTTP GET リクエストを送信 resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() // レスポンスのボディを読み取る body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } // JSONをパースしてmapに変換 var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } return result, nil } // 指定された npm レジストリ、ユーザー名、およびパスワードを使用してnpm 認証トークンを取得する関数です。 func getNpmAuthToken(npmRegistry, npmUser, npmPass string) (string, error) { data := map[string]string{ "name": npmUser, "password": npmPass, } jsonData, err := json.Marshal(data) if err != nil { return "", err } req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/-/user/org.couchdb.user:%s", npmRegistry, npmUser), bytes.NewBuffer(jsonData)) if err != nil { return "", err } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.SetBasicAuth(npmUser, npmPass) client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { return "", fmt.Errorf("failed to get token, status code: %d", resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return "", err } var returnData map[string]interface{} if err := json.Unmarshal(body, &returnData); err != nil { return "", err } token, ok := returnData["token"].(string) if !ok { return "", fmt.Errorf("token not found in response") } return token, nil }