diff --git a/.github/workflows/update-manifests.yml b/.github/workflows/update-manifests.yml deleted file mode 100644 index d5d86dc..0000000 --- a/.github/workflows/update-manifests.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Update Manifests - -on: - schedule: - # Run daily at 6 AM UTC - - cron: '0 6 * * *' - workflow_dispatch: - inputs: - runtime: - description: 'Runtime to update (node, python, ruby, all)' - required: false - default: 'all' - type: choice - options: - - all - - node - - python - - ruby - -permissions: - contents: write - pull-requests: write - -jobs: - update-manifests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: 'go.mod' - - - name: Generate manifests - run: | - RUNTIME="${{ github.event.inputs.runtime || 'all' }}" - echo "Generating manifests for: $RUNTIME" - go run ./scripts/generate-manifests "$RUNTIME" - - - name: Check for changes - id: check-changes - run: | - git diff --quiet src/internal/manifest/data/ || echo "changed=true" >> $GITHUB_OUTPUT - - - name: Create Pull Request - if: steps.check-changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: 'chore(manifest): update runtime manifests' - title: 'chore(manifest): update runtime manifests' - body: | - Automated manifest update from upstream sources. - - This PR was created by the [Update Manifests workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}). - - Please review the changes to ensure the URLs and checksums are correct. - branch: chore/update-manifests - delete-branch: true - labels: | - automated - manifest diff --git a/package.json b/package.json index 8d7ff5a..09c8323 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,7 @@ "build:shim": "go build -v -ldflags=\"-s -w\" -o dist/dtvem-shim.exe ./src/cmd/shim", "deploy:local": "npm run build && copy dist\\dtvem.exe %USERPROFILE%\\.dtvem\\bin\\dtvem.exe && copy dist\\dtvem-shim.exe %USERPROFILE%\\.dtvem\\bin\\dtvem-shim.exe && echo Deploy complete. Run 'dtvem reshim' manually to update shims.", "clean": "rm -rf dist coverage.out coverage.html", - "check": "npm run format && npm run lint && npm run test", - "manifest:node": "go run ./scripts/generate-manifests node", - "manifest:python": "go run ./scripts/generate-manifests python", - "manifest:ruby": "go run ./scripts/generate-manifests ruby", - "manifest:all": "go run ./scripts/generate-manifests all" + "check": "npm run format && npm run lint && npm run test" }, "devDependencies": { "@commitlint/cli": "^19.0.0", diff --git a/scripts/generate-manifests/main.go b/scripts/generate-manifests/main.go deleted file mode 100644 index c3076df..0000000 --- a/scripts/generate-manifests/main.go +++ /dev/null @@ -1,187 +0,0 @@ -// Script to generate manifest files from upstream sources. -// Run with: go run ./scripts/generate-manifests [node|python|ruby|all] -package main - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path/filepath" - "strings" -) - -func main() { - if len(os.Args) < 2 { - fmt.Println("Usage: go run ./scripts/generate-manifests [node|python|ruby|all]") - os.Exit(1) - } - - runtime := os.Args[1] - - // Determine output directory (relative to repo root) - outputDir := "src/internal/manifest/data" - if len(os.Args) > 2 { - outputDir = os.Args[2] - } - - switch runtime { - case "node": - if err := generateNodeManifest(outputDir); err != nil { - fmt.Fprintf(os.Stderr, "Error generating Node.js manifest: %v\n", err) - os.Exit(1) - } - case "python": - if err := generatePythonManifest(outputDir); err != nil { - fmt.Fprintf(os.Stderr, "Error generating Python manifest: %v\n", err) - os.Exit(1) - } - case "ruby": - if err := generateRubyManifest(outputDir); err != nil { - fmt.Fprintf(os.Stderr, "Error generating Ruby manifest: %v\n", err) - os.Exit(1) - } - case "all": - var errors []string - if err := generateNodeManifest(outputDir); err != nil { - errors = append(errors, fmt.Sprintf("Node.js: %v", err)) - } - if err := generatePythonManifest(outputDir); err != nil { - errors = append(errors, fmt.Sprintf("Python: %v", err)) - } - if err := generateRubyManifest(outputDir); err != nil { - errors = append(errors, fmt.Sprintf("Ruby: %v", err)) - } - if len(errors) > 0 { - fmt.Fprintf(os.Stderr, "Errors:\n%s\n", strings.Join(errors, "\n")) - os.Exit(1) - } - default: - fmt.Fprintf(os.Stderr, "Unknown runtime: %s\n", runtime) - os.Exit(1) - } - - fmt.Println("Done!") -} - -// Manifest represents our manifest JSON structure -type Manifest struct { - Schema string `json:"$schema,omitempty"` - Version int `json:"version"` - Versions map[string]map[string]*Download `json:"versions"` -} - -// Download contains URL and SHA256 for a binary -type Download struct { - URL string `json:"url"` - SHA256 string `json:"sha256"` -} - -// githubRelease represents a GitHub release -type githubRelease struct { - TagName string `json:"tag_name"` - Assets []githubAsset `json:"assets"` -} - -// githubAsset represents a GitHub release asset -type githubAsset struct { - Name string `json:"name"` - BrowserDownloadURL string `json:"browser_download_url"` - // SHA256 digest is in the format "sha256:abc123..." - Digest string `json:"digest"` -} - -// writeManifest writes a manifest to a JSON file -func writeManifest(m *Manifest, outputDir, filename string) error { - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) - } - - path := filepath.Join(outputDir, filename) - data, err := json.MarshalIndent(m, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal manifest: %w", err) - } - - // Add trailing newline - data = append(data, '\n') - - if err := os.WriteFile(path, data, 0644); err != nil { - return fmt.Errorf("failed to write manifest: %w", err) - } - - fmt.Printf("Wrote %s\n", path) - return nil -} - -// fetchAllGitHubReleases fetches all releases from a GitHub API URL, paginating through all pages -func fetchAllGitHubReleases(baseURL string) ([]githubRelease, error) { - return fetchGitHubReleasesWithPageSize(baseURL, 100) -} - -// fetchGitHubReleasesWithPageSize fetches all releases with a custom page size -// Smaller page sizes may be needed for repos with large responses (many assets per release) -func fetchGitHubReleasesWithPageSize(baseURL string, pageSize int) ([]githubRelease, error) { - var allReleases []githubRelease - url := fmt.Sprintf("%s?per_page=%d", baseURL, pageSize) - - for url != "" { - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - // GitHub API recommends setting Accept header - req.Header.Set("Accept", "application/vnd.github+json") - req.Header.Set("User-Agent", "dtvem-manifest-generator") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - return nil, fmt.Errorf("HTTP %d", resp.StatusCode) - } - - var releases []githubRelease - if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { - resp.Body.Close() - return nil, err - } - resp.Body.Close() - - allReleases = append(allReleases, releases...) - - // Check for next page in Link header - url = getNextPageURL(resp.Header.Get("Link")) - } - - return allReleases, nil -} - -// getNextPageURL parses the Link header to find the next page URL -// Link header format: ; rel="next", ; rel="last" -func getNextPageURL(linkHeader string) string { - if linkHeader == "" { - return "" - } - - links := strings.Split(linkHeader, ",") - for _, link := range links { - parts := strings.Split(strings.TrimSpace(link), ";") - if len(parts) != 2 { - continue - } - - url := strings.Trim(strings.TrimSpace(parts[0]), "<>") - rel := strings.TrimSpace(parts[1]) - - if rel == `rel="next"` { - return url - } - } - - return "" -} diff --git a/scripts/generate-manifests/node.go b/scripts/generate-manifests/node.go deleted file mode 100644 index 0ee2c53..0000000 --- a/scripts/generate-manifests/node.go +++ /dev/null @@ -1,179 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "net/http" - "strings" -) - -const ( - nodeIndexURL = "https://nodejs.org/dist/index.json" - nodeDistURL = "https://nodejs.org/dist" - nodeSchemaURL = "https://raw.githubusercontent.com/dtvem/dtvem/main/schemas/manifest.schema.json" -) - -// nodeRelease represents a Node.js release from index.json -type nodeRelease struct { - Version string `json:"version"` - Date string `json:"date"` - Files []string `json:"files"` - LTS any `json:"lts"` // false or string - Security bool `json:"security"` -} - -// Platform mappings from Node.js file identifiers to our manifest platform keys -// Node.js uses identifiers like "win-x64-zip", "linux-x64", "osx-arm64-tar" -var nodePlatformMap = map[string]struct { - platform string - archive string -}{ - "win-x64-zip": {"windows-amd64", "zip"}, - "win-arm64-zip": {"windows-arm64", "zip"}, - "win-x86-zip": {"windows-386", "zip"}, - "osx-x64-tar": {"darwin-amd64", "tar.gz"}, - "osx-arm64-tar": {"darwin-arm64", "tar.gz"}, - "linux-x64": {"linux-amd64", "tar.gz"}, - "linux-arm64": {"linux-arm64", "tar.gz"}, - "linux-armv7l": {"linux-arm", "tar.gz"}, -} - -func generateNodeManifest(outputDir string) error { - fmt.Println("Generating Node.js manifest...") - - // Fetch version index - releases, err := fetchNodeReleases() - if err != nil { - return fmt.Errorf("failed to fetch releases: %w", err) - } - - fmt.Printf("Found %d releases\n", len(releases)) - - manifest := &Manifest{ - Schema: nodeSchemaURL, - Version: 1, - Versions: make(map[string]map[string]*Download), - } - - // Process each release - for i, release := range releases { - version := strings.TrimPrefix(release.Version, "v") - - // Progress indicator - if (i+1)%50 == 0 || i == len(releases)-1 { - fmt.Printf("Processing %d/%d versions...\n", i+1, len(releases)) - } - - // Fetch checksums for this version - checksums, err := fetchNodeChecksums(release.Version) - if err != nil { - fmt.Printf("Warning: failed to fetch checksums for %s: %v\n", version, err) - continue - } - - // Build platform map for this version - platforms := make(map[string]*Download) - - for nodeFile, mapping := range nodePlatformMap { - // Check if this platform is available for this version - if !containsFile(release.Files, nodeFile) { - continue - } - - // Construct filename based on platform - var filename string - if strings.HasPrefix(nodeFile, "win-") { - // Windows: node-v22.0.0-win-x64.zip - // nodeFile is "win-x64-zip", we need "win-x64" - winPlatform := strings.TrimSuffix(nodeFile, "-zip") - filename = fmt.Sprintf("node-%s-%s.%s", release.Version, winPlatform, mapping.archive) - } else if strings.HasPrefix(nodeFile, "osx-") { - // macOS: node-v22.0.0-darwin-arm64.tar.gz - // nodeFile is "osx-arm64-tar", we need "arm64" - archPart := strings.TrimSuffix(strings.TrimPrefix(nodeFile, "osx-"), "-tar") - filename = fmt.Sprintf("node-%s-darwin-%s.%s", release.Version, archPart, mapping.archive) - } else { - // Linux: node-v22.0.0-linux-x64.tar.gz - filename = fmt.Sprintf("node-%s-%s.%s", release.Version, nodeFile, mapping.archive) - } - - // Look up checksum (may not exist for all files) - sha256 := checksums[filename] - - platforms[mapping.platform] = &Download{ - URL: fmt.Sprintf("%s/%s/%s", nodeDistURL, release.Version, filename), - SHA256: sha256, - } - } - - if len(platforms) > 0 { - manifest.Versions[version] = platforms - } - } - - fmt.Printf("Generated manifest with %d versions\n", len(manifest.Versions)) - - return writeManifest(manifest, outputDir, "node.json") -} - -func fetchNodeReleases() ([]nodeRelease, error) { - resp, err := http.Get(nodeIndexURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP %d", resp.StatusCode) - } - - var releases []nodeRelease - if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { - return nil, err - } - - return releases, nil -} - -func fetchNodeChecksums(version string) (map[string]string, error) { - url := fmt.Sprintf("%s/%s/SHASUMS256.txt", nodeDistURL, version) - - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("HTTP %d", resp.StatusCode) - } - - checksums := make(map[string]string) - scanner := bufio.NewScanner(resp.Body) - - for scanner.Scan() { - line := scanner.Text() - // Format: - parts := strings.Fields(line) - if len(parts) >= 2 { - sha256 := parts[0] - filename := parts[len(parts)-1] // Handle paths like win-x64/node.exe - // Only include top-level files (not subdirectory files) - if !strings.Contains(filename, "/") { - checksums[filename] = sha256 - } - } - } - - return checksums, scanner.Err() -} - -func containsFile(files []string, target string) bool { - for _, f := range files { - if f == target { - return true - } - } - return false -} diff --git a/scripts/generate-manifests/python.go b/scripts/generate-manifests/python.go deleted file mode 100644 index b780567..0000000 --- a/scripts/generate-manifests/python.go +++ /dev/null @@ -1,232 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "regexp" - "strings" -) - -const ( - pythonReleasesURL = "https://api.github.com/repos/astral-sh/python-build-standalone/releases" - pythonOrgFTPURL = "https://www.python.org/ftp/python/" - pythonSchemaURL = "https://raw.githubusercontent.com/dtvem/dtvem/main/schemas/manifest.schema.json" -) - -// Platform mappings from python-build-standalone naming to our manifest platform keys -// We use the "install_only" variant for simplicity -var pythonPlatformMap = map[string]string{ - "x86_64-pc-windows-msvc": "windows-amd64", - "aarch64-pc-windows-msvc": "windows-arm64", - "x86_64-apple-darwin": "darwin-amd64", - "aarch64-apple-darwin": "darwin-arm64", - "x86_64-unknown-linux-gnu": "linux-amd64", - "aarch64-unknown-linux-gnu": "linux-arm64", -} - -// Regex to parse asset names like: cpython-3.13.1+20251209-x86_64-unknown-linux-gnu-install_only.tar.gz -var pythonAssetRegex = regexp.MustCompile(`^cpython-(\d+\.\d+\.\d+)\+\d+-(.+)-install_only\.tar\.gz$`) - -// Regex to parse python.org version directories -var pythonOrgVersionRegex = regexp.MustCompile(`href="(\d+\.\d+\.\d+)/"`) - -// Regex to parse python.org embeddable package names -var pythonOrgEmbedRegex = regexp.MustCompile(`href="(python-(\d+\.\d+\.\d+)-embed-(amd64|arm64)\.zip)"`) - -func generatePythonManifest(outputDir string) error { - fmt.Println("Generating Python manifest...") - - manifest := &Manifest{ - Schema: pythonSchemaURL, - Version: 1, - Versions: make(map[string]map[string]*Download), - } - - // First, fetch from python-build-standalone (primary source for Linux/macOS, newer Windows) - if err := fetchPythonBuildStandalone(manifest); err != nil { - fmt.Printf("Warning: failed to fetch python-build-standalone: %v\n", err) - } - - // Then, fetch from python.org for Windows (fills gaps for older versions) - if err := fetchPythonOrg(manifest); err != nil { - fmt.Printf("Warning: failed to fetch python.org: %v\n", err) - } - - fmt.Printf("Generated manifest with %d versions\n", len(manifest.Versions)) - - return writeManifest(manifest, outputDir, "python.json") -} - -// fetchPythonBuildStandalone fetches releases from astral-sh/python-build-standalone -func fetchPythonBuildStandalone(manifest *Manifest) error { - fmt.Println("Fetching from python-build-standalone...") - - releases, err := fetchPythonReleases() - if err != nil { - return fmt.Errorf("failed to fetch releases: %w", err) - } - - fmt.Printf("Found %d releases from python-build-standalone\n", len(releases)) - - for _, release := range releases { - for _, asset := range release.Assets { - matches := pythonAssetRegex.FindStringSubmatch(asset.Name) - if matches == nil { - continue - } - - version := matches[1] - pbsPlatform := matches[2] - - platform, ok := pythonPlatformMap[pbsPlatform] - if !ok { - continue - } - - sha256 := "" - if strings.HasPrefix(asset.Digest, "sha256:") { - sha256 = strings.TrimPrefix(asset.Digest, "sha256:") - } - - if manifest.Versions[version] == nil { - manifest.Versions[version] = make(map[string]*Download) - } - - if manifest.Versions[version][platform] == nil { - manifest.Versions[version][platform] = &Download{ - URL: asset.BrowserDownloadURL, - SHA256: sha256, - } - } - } - } - - return nil -} - -// fetchPythonOrg fetches Windows embeddable packages from python.org -func fetchPythonOrg(manifest *Manifest) error { - fmt.Println("Fetching Windows packages from python.org...") - - // Get list of versions from python.org FTP - versions, err := listPythonOrgVersions() - if err != nil { - return fmt.Errorf("failed to list versions: %w", err) - } - - fmt.Printf("Found %d versions on python.org\n", len(versions)) - - addedCount := 0 - for _, version := range versions { - // Skip if we already have Windows builds for this version - if manifest.Versions[version] != nil && manifest.Versions[version]["windows-amd64"] != nil { - continue - } - - // Check for embeddable packages - packages, err := listPythonOrgPackages(version) - if err != nil { - continue // Skip versions without packages - } - - for _, pkg := range packages { - if manifest.Versions[version] == nil { - manifest.Versions[version] = make(map[string]*Download) - } - - // Only add if we don't already have this platform - if manifest.Versions[version][pkg.Platform] == nil { - manifest.Versions[version][pkg.Platform] = &Download{ - URL: pkg.URL, - SHA256: "", // python.org doesn't provide easy SHA256 access - } - addedCount++ - } - } - } - - fmt.Printf("Added %d Windows packages from python.org\n", addedCount) - return nil -} - -// listPythonOrgVersions lists available Python versions from python.org FTP -func listPythonOrgVersions() ([]string, error) { - resp, err := http.Get(pythonOrgFTPURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - matches := pythonOrgVersionRegex.FindAllStringSubmatch(string(body), -1) - versions := make([]string, 0, len(matches)) - for _, m := range matches { - version := m[1] - // Only include Python 3.x versions (skip 2.x) - if strings.HasPrefix(version, "3.") { - versions = append(versions, version) - } - } - - return versions, nil -} - -// pythonOrgPackage represents a package from python.org -type pythonOrgPackage struct { - URL string - Platform string -} - -// listPythonOrgPackages lists embeddable packages for a specific version -func listPythonOrgPackages(version string) ([]pythonOrgPackage, error) { - url := pythonOrgFTPURL + version + "/" - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("HTTP %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - matches := pythonOrgEmbedRegex.FindAllStringSubmatch(string(body), -1) - packages := make([]pythonOrgPackage, 0, len(matches)) - - for _, m := range matches { - filename := m[1] - arch := m[3] - - platform := "" - switch arch { - case "amd64": - platform = "windows-amd64" - case "arm64": - platform = "windows-arm64" - default: - continue - } - - packages = append(packages, pythonOrgPackage{ - URL: url + filename, - Platform: platform, - }) - } - - return packages, nil -} - -func fetchPythonReleases() ([]githubRelease, error) { - // Use smaller page size for python-build-standalone due to large number of assets per release - return fetchGitHubReleasesWithPageSize(pythonReleasesURL, 10) -} diff --git a/scripts/generate-manifests/ruby.go b/scripts/generate-manifests/ruby.go deleted file mode 100644 index c2bf3f6..0000000 --- a/scripts/generate-manifests/ruby.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -import ( - "fmt" - "regexp" - "strings" -) - -const ( - // ruby-builder provides macOS and Linux builds - rubyBuilderReleasesURL = "https://api.github.com/repos/ruby/ruby-builder/releases" - // RubyInstaller provides Windows builds - rubyInstallerReleasesURL = "https://api.github.com/repos/oneclick/rubyinstaller2/releases" - rubySchemaURL = "https://raw.githubusercontent.com/dtvem/dtvem/main/schemas/manifest.schema.json" -) - -// Platform mappings from ruby-builder naming to our manifest platform keys -// Prefer ubuntu-22.04 for broader compatibility -var rubyBuilderPlatformMap = map[string]string{ - "darwin-arm64": "darwin-arm64", - "darwin-x64": "darwin-amd64", - "ubuntu-22.04-x64": "linux-amd64", - "ubuntu-22.04-arm64": "linux-arm64", - // Fallback to ubuntu-24.04 if 22.04 not available - "ubuntu-24.04-x64": "linux-amd64", - "ubuntu-24.04-arm64": "linux-arm64", -} - -// Platform mappings from RubyInstaller naming to our manifest platform keys -var rubyInstallerPlatformMap = map[string]string{ - "x64": "windows-amd64", - // x86 builds are not included (32-bit Windows) -} - -// Regex to parse ruby-builder asset names like: ruby-3.3.10-ubuntu-22.04-x64.tar.gz -// Captures: version, suffix type, platform (e.g., "ubuntu-22.04-x64" or "darwin-arm64") -// The version suffix only matches known pre-release patterns (preview, rc, alpha, beta, dev) -// to avoid capturing platform names like "darwin" -var rubyBuilderAssetRegex = regexp.MustCompile(`^ruby-(\d+\.\d+\.\d+(?:-(preview|rc|alpha|beta|dev)\d*)?)-(.+)\.tar\.gz$`) - -// Regex to parse RubyInstaller asset names like: rubyinstaller-3.3.10-1-x64.7z -// Captures: version, build number (unused), architecture -var rubyInstallerAssetRegex = regexp.MustCompile(`^rubyinstaller-(\d+\.\d+\.\d+)-\d+-(\w+)\.7z$`) - -func generateRubyManifest(outputDir string) error { - fmt.Println("Generating Ruby manifest...") - - manifest := &Manifest{ - Schema: rubySchemaURL, - Version: 1, - Versions: make(map[string]map[string]*Download), - } - - // Track which version/platform combos we've already added - // to prefer ubuntu-22.04 over ubuntu-24.04 - added := make(map[string]bool) - - // Fetch and process ruby-builder releases (macOS + Linux) - builderReleases, err := fetchRubyBuilderReleases() - if err != nil { - return fmt.Errorf("failed to fetch ruby-builder releases: %w", err) - } - fmt.Printf("Found %d ruby-builder releases\n", len(builderReleases)) - processRubyBuilderReleases(builderReleases, manifest, added) - - // Fetch and process RubyInstaller releases (Windows) - installerReleases, err := fetchRubyInstallerReleases() - if err != nil { - return fmt.Errorf("failed to fetch RubyInstaller releases: %w", err) - } - fmt.Printf("Found %d RubyInstaller releases\n", len(installerReleases)) - processRubyInstallerReleases(installerReleases, manifest, added) - - fmt.Printf("Generated manifest with %d versions\n", len(manifest.Versions)) - - return writeManifest(manifest, outputDir, "ruby.json") -} - -// processRubyBuilderReleases processes ruby-builder releases for macOS and Linux -func processRubyBuilderReleases(releases []githubRelease, manifest *Manifest, added map[string]bool) { - for _, release := range releases { - for _, asset := range release.Assets { - // Skip non-standard Ruby builds (truffleruby, jruby, etc.) - if !strings.HasPrefix(asset.Name, "ruby-") { - continue - } - - // Parse asset name to extract version and platform - matches := rubyBuilderAssetRegex.FindStringSubmatch(asset.Name) - if matches == nil { - continue - } - - version := matches[1] // e.g., "3.3.10" or "4.0.0-preview2" - // matches[2] is the suffix type (preview, rc, etc.) - not used - rbPlatform := matches[3] // e.g., "ubuntu-22.04-x64" - - // Map to our platform key - platform, ok := rubyBuilderPlatformMap[rbPlatform] - if !ok { - continue - } - - // Extract SHA256 from digest if available (format: "sha256:abc123...") - sha256 := "" - if strings.HasPrefix(asset.Digest, "sha256:") { - sha256 = strings.TrimPrefix(asset.Digest, "sha256:") - } - - // Initialize version map if needed - if manifest.Versions[version] == nil { - manifest.Versions[version] = make(map[string]*Download) - } - - // Create a unique key for tracking - key := version + "/" + platform - - // Only add if we don't already have this version/platform - // This ensures we prefer ubuntu-22.04 over ubuntu-24.04 (processed first) - if !added[key] { - manifest.Versions[version][platform] = &Download{ - URL: asset.BrowserDownloadURL, - SHA256: sha256, - } - added[key] = true - } - } - } -} - -// processRubyInstallerReleases processes RubyInstaller releases for Windows -// Note: Older RubyInstaller releases don't have SHA256 digests (artifact attestations), -// but we still include them for broader Windows support -func processRubyInstallerReleases(releases []githubRelease, manifest *Manifest, added map[string]bool) { - for _, release := range releases { - for _, asset := range release.Assets { - // Parse asset name to extract version and architecture - matches := rubyInstallerAssetRegex.FindStringSubmatch(asset.Name) - if matches == nil { - continue - } - - version := matches[1] // e.g., "3.3.10" - arch := matches[2] // e.g., "x64" - - // Map to our platform key - platform, ok := rubyInstallerPlatformMap[arch] - if !ok { - continue - } - - // Extract SHA256 from digest if available (format: "sha256:abc123...") - // Note: Only recent releases have artifact attestations with digests - sha256 := "" - if strings.HasPrefix(asset.Digest, "sha256:") { - sha256 = strings.TrimPrefix(asset.Digest, "sha256:") - } - - // Initialize version map if needed - if manifest.Versions[version] == nil { - manifest.Versions[version] = make(map[string]*Download) - } - - // Create a unique key for tracking - key := version + "/" + platform - - // Only add if we don't already have this version/platform - if !added[key] { - manifest.Versions[version][platform] = &Download{ - URL: asset.BrowserDownloadURL, - SHA256: sha256, - } - added[key] = true - } - } - } -} - -func fetchRubyBuilderReleases() ([]githubRelease, error) { - return fetchAllGitHubReleases(rubyBuilderReleasesURL) -} - -func fetchRubyInstallerReleases() ([]githubRelease, error) { - return fetchAllGitHubReleases(rubyInstallerReleasesURL) -}