This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git


The following commit(s) were added to refs/heads/main by this push:
     new 3655e78  Ruby dependency license scanning support via Gemfile.lock. 
(#205)
3655e78 is described below

commit 3655e78854be6a56ae72a0c9a847aedd133c3b74
Author: |7eter l-|. l3oling <[email protected]>
AuthorDate: Thu Sep 11 04:01:53 2025 -0600

    Ruby dependency license scanning support via Gemfile.lock. (#205)
    
    * Ruby dependency license scanning support via Gemfile.lock.
    
    - Implements https://github.com/apache/skywalking/issues/7744
    - Library projects (with a *.gemspec in the same directory as Gemfile.lock) 
ignore development dependencies and include runtime dependencies and their 
transitives.
    - App projects (no *.gemspec) include both runtime and development 
dependencies from Gemfile.lock.
    - Will only work if Gemfile.lock is committed to version control, but this 
is the official recommendation of RubyGems:
      - https://bundler.io/guides/faq.html#using-gemfiles-inside-gems
    - License resolution honors user overrides/exclusions and may query the 
RubyGems API when necessary, with proper support for handling of various status 
codes.
    - Documentation updated (README.md)
      - Ruby setup and GitHub Actions example are in <details> tag to reduce 
noise
---
 .licenserc.yaml                               |   2 +
 README.md                                     |  35 +++
 pkg/deps/resolve.go                           |   1 +
 pkg/deps/ruby.go                              | 415 ++++++++++++++++++++++++++
 pkg/deps/ruby_test.go                         | 120 ++++++++
 pkg/deps/testdata/ruby/app/Gemfile.lock       |  17 ++
 pkg/deps/testdata/ruby/library/Gemfile.lock   |  17 ++
 pkg/deps/testdata/ruby/library/sample.gemspec |  28 ++
 8 files changed, 635 insertions(+)

diff --git a/.licenserc.yaml b/.licenserc.yaml
index 8acde6a..8f3576b 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -76,6 +76,8 @@ header: # `header` section is configurations for source codes 
license header.
     - "**/assets/assets.gen.go"
     - "docs/**.svg"
     - "pkg/gitignore/dir.go"
+    - "pkg/deps/testdata/ruby/app/Gemfile.lock"
+    - "pkg/deps/testdata/ruby/library/Gemfile.lock"
 
   comment: on-failure # on what condition license-eye will comment on the pull 
request, `on-failure`, `always`, `never`.
 
diff --git a/README.md b/README.md
index c55e8f7..c1946f8 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ dependency:
     - Cargo.toml        # If this is a rust project.
     - package.json      # If this is a npm project.
     - go.mod            # If this is a Go project.
+    - Gemfile.lock      # If this is a Ruby project (Bundler). Ensure 
Gemfile.lock is committed.
 ```
 
 #### Check License Headers
@@ -102,6 +103,40 @@ To check dependencies license in GitHub Actions, add a 
step in your GitHub workf
       # flags: # optional: Extra flags appended to the command, for example, 
`--summary=path/to/template.tmpl`
 ```
 
+<details>
+<summary>Ruby projects (Bundler)</summary>
+
+License-Eye can resolve Ruby dependencies and their licenses directly from 
Gemfile.lock.
+
+Rules applied:
+- If a .gemspec file exists in the same directory as Gemfile.lock, the project 
is treated as a library and development dependencies are ignored. Runtime 
dependencies (and their transitives) are included.
+- If no .gemspec is present, the project is treated as an app and all 
dependencies from Gemfile.lock are considered (both runtime and development).
+
+Requirements:
+- Commit Gemfile.lock to version control so License-Eye can read the locked 
dependency graph.
+- For libraries, ensure the .gemspec is present in the same directory as 
Gemfile.lock.
+
+Minimal config snippet:
+
+```yaml
+dependency:
+  files:
+    - Gemfile.lock
+```
+
+GitHub Actions example:
+
+```yaml
+- name: Check Ruby dependencies' licenses
+  uses: apache/skywalking-eyes/dependency@main
+  with:
+    config: .licenserc.yaml
+```
+
+Note: License-Eye may query the RubyGems API to determine licenses when they 
are not specified in your configuration. Ensure the workflow has network access.
+
+</details>
+
 ### Docker Image
 
 For Bash, users can execute the following command,
diff --git a/pkg/deps/resolve.go b/pkg/deps/resolve.go
index fc2f562..2551b6a 100644
--- a/pkg/deps/resolve.go
+++ b/pkg/deps/resolve.go
@@ -32,6 +32,7 @@ var Resolvers = []Resolver{
        new(MavenPomResolver),
        new(JarResolver),
        new(CargoTomlResolver),
+       new(GemfileLockResolver),
 }
 
 func Resolve(config *ConfigDeps, report *Report) error {
diff --git a/pkg/deps/ruby.go b/pkg/deps/ruby.go
new file mode 100644
index 0000000..5bbe5a5
--- /dev/null
+++ b/pkg/deps/ruby.go
@@ -0,0 +1,415 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps
+
+import (
+       "bufio"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strconv"
+       "strings"
+       "time"
+)
+
+// GemfileLockResolver resolves Ruby dependencies from Gemfile.lock
+// It determines project type by the presence of a *.gemspec file in the same 
directory as Gemfile.lock.
+// - Library projects (with gemspec): ignore development dependencies; include 
only runtime deps and their transitive closure.
+// - App projects (no gemspec): include all dependencies in Gemfile.lock.
+// Licenses are fetched from RubyGems API unless overridden by user config.
+// See issue description for detailed rules.
+
+type GemfileLockResolver struct {
+       Resolver
+}
+
+func (r *GemfileLockResolver) CanResolve(file string) bool {
+       base := filepath.Base(file)
+       return base == "Gemfile.lock"
+}
+
+func (r *GemfileLockResolver) Resolve(lockfile string, config *ConfigDeps, 
report *Report) error {
+       dir := filepath.Dir(lockfile)
+
+       content, err := os.ReadFile(lockfile)
+       if err != nil {
+               return err
+       }
+
+       // Parse lockfile into specs graph and top-level dependencies
+       specs, deps, err := parseGemfileLock(string(content))
+       if err != nil {
+               return err
+       }
+
+       isLibrary := hasGemspec(dir)
+
+       var roots []string
+       if isLibrary {
+               // Extract runtime dependencies from gemspec(s)
+               runtimeRoots, err := runtimeDepsFromGemspecs(dir)
+               if err != nil {
+                       return err
+               }
+               if len(runtimeRoots) == 0 {
+                       // Fallback: if not found, use DEPENDENCIES from 
lockfile
+                       roots = deps
+               } else {
+                       roots = runtimeRoots
+               }
+       } else {
+               // App: all declared dependencies are relevant
+               roots = deps
+       }
+
+       // Compute the set of included gems
+       include := reachable(specs, roots)
+       // For app without explicit deps (rare), include all specs
+       if len(roots) == 0 {
+               for name := range specs {
+                       include[name] = struct{}{}
+               }
+       }
+
+       // Resolve licenses for included gems
+       for name := range include {
+               version := specs[name].Version
+               if exclude, _ := config.IsExcluded(name, version); exclude {
+                       continue
+               }
+               if l, ok := config.GetUserConfiguredLicense(name, version); ok {
+                       report.Resolve(&Result{Dependency: name, LicenseSpdxID: 
l, Version: version})
+                       continue
+               }
+
+               licenseID, err := fetchRubyGemsLicense(name, version)
+               if err != nil || licenseID == "" {
+                       report.Skip(&Result{Dependency: name, LicenseSpdxID: 
Unknown, Version: version})
+                       continue
+               }
+               report.Resolve(&Result{Dependency: name, LicenseSpdxID: 
licenseID, Version: version})
+       }
+
+       return nil
+}
+
+// -------- Parsing Gemfile.lock --------
+
+type gemSpec struct {
+       Name    string
+       Version string
+       Deps    []string
+}
+
+type gemGraph map[string]*gemSpec
+
+var (
+       lockSpecHeader = regexp.MustCompile(`^\s{4}([a-zA-Z0-9_\-]+) 
\(([^)]+)\)`) //     rake (13.0.6)
+       lockDepLine    = regexp.MustCompile(`^\s{6}([a-zA-Z0-9_\-]+)(?:\s|$)`)  
   //       activesupport (~> 6.1)
+)
+
+func parseGemfileLock(s string) (graph gemGraph, roots []string, err error) {
+       scanner := bufio.NewScanner(strings.NewReader(s))
+       scanner.Split(bufio.ScanLines)
+       graph = make(gemGraph)
+
+       inSpecs := false
+       inDeps := false
+       var current *gemSpec
+
+       for scanner.Scan() {
+               line := scanner.Text()
+               if strings.HasPrefix(line, "GEM") {
+                       inSpecs = true
+                       inDeps = false
+                       current = nil
+                       continue
+               }
+               if strings.HasPrefix(line, "DEPENDENCIES") {
+                       inSpecs = false
+                       inDeps = true
+                       current = nil
+                       continue
+               }
+               if strings.TrimSpace(line) == "specs:" && inSpecs {
+                       // just a marker
+                       continue
+               }
+
+               if inSpecs {
+                       if m := lockSpecHeader.FindStringSubmatch(line); len(m) 
== 3 {
+                               name := m[1]
+                               version := m[2]
+                               current = &gemSpec{Name: name, Version: version}
+                               graph[name] = current
+                               continue
+                       }
+                       if current != nil {
+                               if m := lockDepLine.FindStringSubmatch(line); 
len(m) == 2 {
+                                       depName := m[1]
+                                       current.Deps = append(current.Deps, 
depName)
+                               }
+                       }
+                       continue
+               }
+
+               if inDeps {
+                       trim := strings.TrimSpace(line)
+                       if trim == "" || strings.HasPrefix(trim, "BUNDLED 
WITH") {
+                               inDeps = false
+                               continue
+                       }
+                       // dependency line: byebug (~> 11.1)
+                       root := trim
+                       if i := strings.Index(root, " "); i >= 0 {
+                               root = root[:i]
+                       }
+                       // ignore comments and platforms
+                       if root != "" && !strings.HasPrefix(root, "#") {
+                               roots = append(roots, root)
+                       }
+                       continue
+               }
+       }
+       if err := scanner.Err(); err != nil {
+               return nil, nil, err
+       }
+       return graph, roots, nil
+}
+
+func hasGemspec(dir string) bool {
+       entries, err := os.ReadDir(dir)
+       if err != nil {
+               return false
+       }
+       for _, e := range entries {
+               if !e.IsDir() && strings.HasSuffix(e.Name(), ".gemspec") {
+                       return true
+               }
+       }
+       return false
+}
+
+var gemspecRuntimeRe = 
regexp.MustCompile(`(?m)\badd_(?:runtime_)?dependency\s*\(?\s*["']([^"']+)["']`)
+
+func runtimeDepsFromGemspecs(dir string) ([]string, error) {
+       entries, err := os.ReadDir(dir)
+       if err != nil {
+               return nil, err
+       }
+       runtime := make(map[string]struct{})
+       for _, e := range entries {
+               if e.IsDir() || !strings.HasSuffix(e.Name(), ".gemspec") {
+                       continue
+               }
+               b, err := os.ReadFile(filepath.Join(dir, e.Name()))
+               if err != nil {
+                       return nil, err
+               }
+               for _, m := range 
gemspecRuntimeRe.FindAllStringSubmatch(string(b), -1) {
+                       if len(m) == 2 {
+                               runtime[m[1]] = struct{}{}
+                       }
+               }
+       }
+       res := make([]string, 0, len(runtime))
+       for k := range runtime {
+               res = append(res, k)
+       }
+       return res, nil
+}
+
+func reachable(graph gemGraph, roots []string) map[string]struct{} {
+       vis := make(map[string]struct{})
+       var dfs func(string)
+       dfs = func(n string) {
+               if _, ok := vis[n]; ok {
+                       return
+               }
+               if _, ok := graph[n]; !ok {
+                       // unknown in specs, still include the root
+                       vis[n] = struct{}{}
+                       return
+               }
+               vis[n] = struct{}{}
+               for _, c := range graph[n].Deps {
+                       dfs(c)
+               }
+       }
+       for _, r := range roots {
+               dfs(r)
+       }
+       return vis
+}
+
+// -------- License resolution via RubyGems API --------
+
+type rubyGemsVersionInfo struct {
+       Licenses []string `json:"licenses"`
+       License  string   `json:"license"`
+}
+
+func fetchRubyGemsLicense(name, version string) (string, error) {
+       // Prefer version-specific API
+       url := 
fmt.Sprintf("https://rubygems.org/api/v2/rubygems/%s/versions/%s.json";, name, 
version)
+       licenseID, err := fetchRubyGemsLicenseFrom(url)
+       if err == nil && licenseID != "" {
+               return licenseID, nil
+       }
+       // Fallback to latest info
+       url = fmt.Sprintf("https://rubygems.org/api/v1/gems/%s.json";, name)
+       return fetchRubyGemsLicenseFrom(url)
+}
+
+var httpClientRuby = &http.Client{Timeout: 10 * time.Second}
+
+func fetchRubyGemsLicenseFrom(url string) (string, error) {
+       const maxAttempts = 3
+       backoff := 1 * time.Second
+
+       for attempt := 1; attempt <= maxAttempts; attempt++ {
+               req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
+               if err != nil {
+                       return "", err
+               }
+               req.Header.Set("User-Agent", "skywalking-eyes/License-Eye 
(+https://github.com/apache/skywalking-eyes)")
+               req.Header.Set("Accept", "application/json")
+
+               resp, err := httpClientRuby.Do(req) // #nosec G107
+               if err != nil {
+                       if attempt == maxAttempts {
+                               return "", err
+                       }
+                       time.Sleep(backoff)
+                       backoff *= 2
+                       continue
+               }
+
+               license, wait, retry, hErr := handleRubyGemsResponse(resp)
+               _ = resp.Body.Close()
+
+               if hErr != nil {
+                       if retry && attempt < maxAttempts {
+                               if wait > 0 {
+                                       time.Sleep(wait)
+                               } else {
+                                       time.Sleep(backoff)
+                               }
+                               backoff *= 2
+                               continue
+                       }
+                       return "", hErr
+               }
+
+               if retry { // safety branch, normally handled above when hErr 
!= nil
+                       if attempt == maxAttempts {
+                               return "", fmt.Errorf("max attempts reached")
+                       }
+                       if wait > 0 {
+                               time.Sleep(wait)
+                       } else {
+                               time.Sleep(backoff)
+                       }
+                       backoff *= 2
+                       continue
+               }
+
+               return license, nil
+       }
+       return "", nil
+}
+
+func handleRubyGemsResponse(resp *http.Response) (license string, wait 
time.Duration, retry bool, err error) {
+       switch {
+       case resp.StatusCode == http.StatusOK:
+               license, err := parseRubyGemsLicenseJSON(resp.Body)
+               return license, 0, false, err
+       case resp.StatusCode == http.StatusNotFound:
+               // Treat as no license info available
+               return "", 0, false, nil
+       case resp.StatusCode == http.StatusTooManyRequests || (resp.StatusCode 
>= 500 && resp.StatusCode <= 599):
+               wait := retryAfterDuration(resp.Header.Get("Retry-After"))
+               return "", wait, true, fmt.Errorf("retryable status: %s", 
resp.Status)
+       default:
+               return "", 0, false, fmt.Errorf("unexpected status: %s", 
resp.Status)
+       }
+}
+
+func parseRubyGemsLicenseJSON(r io.Reader) (string, error) {
+       var info rubyGemsVersionInfo
+       dec := json.NewDecoder(r)
+       if err := dec.Decode(&info); err != nil {
+               return "", err
+       }
+       var items []string
+       if len(info.Licenses) > 0 {
+               items = info.Licenses
+       } else if info.License != "" {
+               items = []string{info.License}
+       }
+       for i := range items {
+               items[i] = strings.TrimSpace(items[i])
+       }
+       m := make(map[string]struct{})
+       for _, it := range items {
+               if it == "" {
+                       continue
+               }
+               m[it] = struct{}{}
+       }
+       if len(m) == 0 {
+               return "", nil
+       }
+       var out []string
+       for k := range m {
+               out = append(out, k)
+       }
+       slicesSort(out)
+       return strings.Join(out, " OR "), nil
+}
+
+func retryAfterDuration(v string) time.Duration {
+       v = strings.TrimSpace(v)
+       if v == "" {
+               return 0
+       }
+       if secs, err := strconv.Atoi(v); err == nil {
+               wait := time.Duration(secs) * time.Second
+               if wait > 10*time.Second {
+                       wait = 10 * time.Second
+               }
+               return wait
+       }
+       return 0
+}
+
+// small helper to sort string slice without importing sort here to keep 
imports aligned with style used in this package
+func slicesSort(ss []string) {
+       // simple insertion sort for small slices
+       for i := 1; i < len(ss); i++ {
+               j := i
+               for j > 0 && ss[j-1] > ss[j] {
+                       ss[j-1], ss[j] = ss[j], ss[j-1]
+                       j--
+               }
+       }
+}
diff --git a/pkg/deps/ruby_test.go b/pkg/deps/ruby_test.go
new file mode 100644
index 0000000..889a73d
--- /dev/null
+++ b/pkg/deps/ruby_test.go
@@ -0,0 +1,120 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps_test
+
+import (
+       "bufio"
+       "embed"
+       "io/fs"
+       "os"
+       "path/filepath"
+       "strings"
+       "testing"
+
+       "github.com/apache/skywalking-eyes/pkg/deps"
+)
+
+func writeFileRuby(fileName, content string) error {
+       file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 
0777)
+       if err != nil {
+               return err
+       }
+       defer func() { _ = file.Close() }()
+
+       write := bufio.NewWriter(file)
+       _, err = write.WriteString(content)
+       if err != nil {
+               return err
+       }
+       _ = write.Flush()
+       return nil
+}
+
+func ensureDirRuby(dirName string) error {
+       return os.MkdirAll(dirName, 0777)
+}
+
+//go:embed testdata/ruby/**/*
+var rubyTestAssets embed.FS
+
+func copyRuby(assetDir, destination string) error {
+       return fs.WalkDir(rubyTestAssets, assetDir, func(path string, d 
fs.DirEntry, err error) error {
+               if err != nil {
+                       return err
+               }
+               if d.IsDir() {
+                       return nil
+               }
+               filename := filepath.Join(destination, strings.Replace(path, 
assetDir, "", 1))
+               if err := ensureDirRuby(filepath.Dir(filename)); err != nil {
+                       return err
+               }
+               content, err := rubyTestAssets.ReadFile(path)
+               if err != nil {
+                       return err
+               }
+               return writeFileRuby(filename, string(content))
+       })
+}
+
+func TestRubyGemfileLockResolver(t *testing.T) {
+       resolver := new(deps.GemfileLockResolver)
+
+       // App case: include all specs (3)
+       {
+               tmp := t.TempDir()
+               if err := copyRuby("testdata/ruby/app", tmp); err != nil {
+                       t.Fatal(err)
+               }
+               lock := filepath.Join(tmp, "Gemfile.lock")
+               if !resolver.CanResolve(lock) {
+                       t.Fatalf("GemfileLockResolver cannot resolve %s", lock)
+               }
+               cfg := &deps.ConfigDeps{Files: []string{lock}, Licenses: 
[]*deps.ConfigDepLicense{
+                       {Name: "rake", Version: "13.0.6", License: "MIT"},
+                       {Name: "rspec", Version: "3.10.0", License: "MIT"},
+                       {Name: "rspec-core", Version: "3.10.1", License: "MIT"},
+               }}
+               report := deps.Report{}
+               if err := resolver.Resolve(lock, cfg, &report); err != nil {
+                       t.Fatal(err)
+               }
+               if len(report.Resolved)+len(report.Skipped) != 3 {
+                       t.Fatalf("expected 3 dependencies, got %d", 
len(report.Resolved)+len(report.Skipped))
+               }
+       }
+
+       // Library case: only runtime deps reachable from gemspec (1: rake)
+       {
+               tmp := t.TempDir()
+               if err := copyRuby("testdata/ruby/library", tmp); err != nil {
+                       t.Fatal(err)
+               }
+               lock := filepath.Join(tmp, "Gemfile.lock")
+               cfg := &deps.ConfigDeps{Files: []string{lock}, Licenses: 
[]*deps.ConfigDepLicense{
+                       {Name: "rake", Version: "13.0.6", License: "MIT"},
+               }}
+               report := deps.Report{}
+               if err := resolver.Resolve(lock, cfg, &report); err != nil {
+                       t.Fatal(err)
+               }
+               if len(report.Resolved)+len(report.Skipped) != 1 {
+                       t.Fatalf("expected 1 dependency for library, got %d", 
len(report.Resolved)+len(report.Skipped))
+               }
+       }
+}
diff --git a/pkg/deps/testdata/ruby/app/Gemfile.lock 
b/pkg/deps/testdata/ruby/app/Gemfile.lock
new file mode 100644
index 0000000..7d6478b
--- /dev/null
+++ b/pkg/deps/testdata/ruby/app/Gemfile.lock
@@ -0,0 +1,17 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rake (13.0.6)
+    rspec (3.10.0)
+      rspec-core (~> 3.10)
+    rspec-core (3.10.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  rake
+  rspec
+
+BUNDLED WITH
+   2.4.10
diff --git a/pkg/deps/testdata/ruby/library/Gemfile.lock 
b/pkg/deps/testdata/ruby/library/Gemfile.lock
new file mode 100644
index 0000000..7d6478b
--- /dev/null
+++ b/pkg/deps/testdata/ruby/library/Gemfile.lock
@@ -0,0 +1,17 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    rake (13.0.6)
+    rspec (3.10.0)
+      rspec-core (~> 3.10)
+    rspec-core (3.10.1)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  rake
+  rspec
+
+BUNDLED WITH
+   2.4.10
diff --git a/pkg/deps/testdata/ruby/library/sample.gemspec 
b/pkg/deps/testdata/ruby/library/sample.gemspec
new file mode 100644
index 0000000..9ff595d
--- /dev/null
+++ b/pkg/deps/testdata/ruby/library/sample.gemspec
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+Gem::Specification.new do |spec|
+  spec.name          = "sample"
+  spec.version       = "0.1.0"
+  spec.summary       = "Sample gem"
+  spec.description   = "Sample"
+  spec.authors       = ["Test"]
+  spec.files         = []
+
+  spec.add_runtime_dependency 'rake', '>= 13.0'
+  spec.add_development_dependency 'rspec', '~> 3.10'
+end

Reply via email to