diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 770a0ff6..852b3121 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,13 @@ version: 2 updates: + - package-ecosystem: "gomod" + directory: "/scripts/generate-buildpack-docs" + schedule: + interval: "weekly" + groups: + go: + patterns: + - "*" - package-ecosystem: "docker" directory: "/runimage" schedule: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..535d5234 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,60 @@ +name: Update Documentation + +on: + workflow_dispatch: + workflow_call: + +jobs: + update-docs: + runs-on: ubuntu-latest + env: + TOOL_PATH: runway-buildpack-stack/scripts/generate-buildpack-docs + DOC_PATH: www/content/docs/platform/buildpacks/included-buildpacks + steps: + - id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.RUNWAY_CI_BOT_APP_ID }} + private-key: ${{ secrets.RUNWAY_CI_BOT_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: | + runway-buildpack-stack + ${{ secrets.WWW_REPO }} + - id: get-user-id + run: echo "user-id=$(gh api "/users/${{ steps.generate-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + + - uses: actions/checkout@v6 + with: + path: runway-buildpack-stack + + - uses: actions/setup-go@v5 + with: + go-version-file: ${{ env.TOOL_PATH }}/go.mod + + - name: Generate buildpack documentation + working-directory: ${{ env.TOOL_PATH }} + run: | + go run main.go + + - uses: actions/checkout@v6 + with: + repository: ${{ github.repository_owner }}/${{ secrets.WWW_REPO }} + token: ${{ steps.generate-token.outputs.token }} + path: www + + - run: | + mkdir -p ${{ env.DOC_PATH }} + cp -r runway-buildpack-stack/bp-docs/* ${{ env.DOC_PATH }} + + - uses: peter-evans/create-pull-request@v7 + with: + token: ${{ steps.generate-token.outputs.token }} + path: www + commit-message: "chore(docs): update buildpacks" + title: "chore(docs): update buildpacks" + branch: update-buildpack-docs + delete-branch: true + committer: ${{ steps.generate-token.outputs.app-slug }}[bot] <${{ steps.get-user-id.outputs.user-id }}+${{ steps.generate-token.outputs.app-slug }}[bot]@users.noreply.github.com> + author: ${{ steps.generate-token.outputs.app-slug }}[bot] <${{ steps.get-user-id.outputs.user-id }}+${{ steps.generate-token.outputs.app-slug }}[bot]@users.noreply.github.com> diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e9f46de9..3eca0cef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,3 +170,8 @@ jobs: - uses: softprops/action-gh-release@v2 with: token: ${{ steps.generate-token.outputs.token }} + + docs: + needs: [release] + uses: ./.github/workflows/docs.yml + secrets: inherit diff --git a/.gitignore b/.gitignore index ead3b0d7..b5d2e21a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ meta/*/buildpack.cnb +bp-docs/* \ No newline at end of file diff --git a/meta/dotnet-core/buildpack.toml b/meta/dotnet-core/buildpack.toml index f5f78924..1df069d0 100644 --- a/meta/dotnet-core/buildpack.toml +++ b/meta/dotnet-core/buildpack.toml @@ -5,7 +5,7 @@ api = "0.8" homepage = "https://www.runway.horse/docs/recipes/deploy-dotnet/" id = "runway-buildpacks/dotnet-core" keywords = ["dotnet"] - name = "Paketo Buildpack for .NET Core" + name = "Buildpack for .NET Core" version = "3.1.33" [[buildpack.licenses]] diff --git a/meta/go/buildpack.toml b/meta/go/buildpack.toml index 8e782f51..6581430a 100644 --- a/meta/go/buildpack.toml +++ b/meta/go/buildpack.toml @@ -5,7 +5,7 @@ api = "0.7" homepage = "https://www.runway.horse/docs/recipes/deploy-go/" id = "runway-buildpacks/go" keywords = ["go", "golang"] - name = "Runway version of the Paketo Buildpack for Go" + name = "Buildpack for Go" version = "4.13.36" [[buildpack.licenses]] diff --git a/meta/java-native-image/buildpack.toml b/meta/java-native-image/buildpack.toml index 5b87fcee..8cce21ab 100644 --- a/meta/java-native-image/buildpack.toml +++ b/meta/java-native-image/buildpack.toml @@ -5,7 +5,7 @@ api = "0.7" homepage = "https://www.runway.horse/docs/recipes/java/" id = "runway-buildpacks/java-native-image" keywords = ["java", "composite", "native-image"] - name = "Paketo Buildpack for Java Native Image" + name = "Buildpack for Java Native Image" version = "11.15.29" [[buildpack.licenses]] diff --git a/meta/java/buildpack.toml b/meta/java/buildpack.toml index d53c0765..e8e4b3c8 100644 --- a/meta/java/buildpack.toml +++ b/meta/java/buildpack.toml @@ -5,7 +5,7 @@ api = "0.7" homepage = "https://www.runway.horse/docs/recipes/java/" id = "runway-buildpacks/java" keywords = ["java", "composite"] - name = "Paketo Buildpack for Java" + name = "Buildpack for Java" version = "18.12.47" [[buildpack.licenses]] diff --git a/meta/nodejs/buildpack.toml b/meta/nodejs/buildpack.toml index 18ebbbeb..0ac3684b 100644 --- a/meta/nodejs/buildpack.toml +++ b/meta/nodejs/buildpack.toml @@ -3,7 +3,7 @@ api = "0.7" [buildpack] homepage = "https://www.runway.horse/docs/recipes/javascript/" id = "runway-buildpacks/nodejs" - name = "Paketo Buildpack for Node.js" + name = "Buildpack for Node.js" version = "9.3.24" [[buildpack.licenses]] diff --git a/meta/php/buildpack.toml b/meta/php/buildpack.toml index 2cea4e13..bbbb8cf0 100644 --- a/meta/php/buildpack.toml +++ b/meta/php/buildpack.toml @@ -3,7 +3,7 @@ api = "0.7" [buildpack] homepage = "https://github.com/paketo-buildpacks/php" id = "runway-buildpacks/php" - name = "Paketo Buildpack for PHP" + name = "Buildpack for PHP" version = "2.19.26" [[buildpack.licenses]] diff --git a/meta/python/buildpack.toml b/meta/python/buildpack.toml index 001a0510..c04aa8af 100644 --- a/meta/python/buildpack.toml +++ b/meta/python/buildpack.toml @@ -2,7 +2,7 @@ api = "0.8" [buildpack] id = "runway-buildpacks/python" - name = "Paketo Buildpack for Python" + name = "Buildpack for Python" version = "2.30.24" [[buildpack.licenses]] diff --git a/meta/ruby/buildpack.toml b/meta/ruby/buildpack.toml index 53915d72..e70ed16d 100644 --- a/meta/ruby/buildpack.toml +++ b/meta/ruby/buildpack.toml @@ -5,7 +5,7 @@ api = "0.7" homepage = "https://www.runway.horse/docs/recipes/deploy-ruby/" id = "runway-buildpacks/ruby" keywords = ["ruby"] - name = "Paketo Buildpack for Ruby" + name = "Buildpack for Ruby" version = "0.47.32" [[buildpack.licenses]] diff --git a/meta/runway/buildpack.toml b/meta/runway/buildpack.toml index 297bec88..fd37da8f 100644 --- a/meta/runway/buildpack.toml +++ b/meta/runway/buildpack.toml @@ -2,7 +2,7 @@ api = "0.7" [buildpack] id = "runway-buildpacks/runway" - name = "Composite buildpack to include our own buildpacks" + name = "Buildpack for hugo/mdbook" version = "0.1.27" [[order]] diff --git a/scripts/generate-buildpack-docs/go.mod b/scripts/generate-buildpack-docs/go.mod new file mode 100644 index 00000000..e299ce95 --- /dev/null +++ b/scripts/generate-buildpack-docs/go.mod @@ -0,0 +1,13 @@ +module generate-buildpack-docs + +go 1.25.4 + +require ( + github.com/BurntSushi/toml v1.6.0 + github.com/buildpacks/libcnb/v2 v2.1.0 +) + +require ( + github.com/Masterminds/semver v1.5.0 // indirect + github.com/onsi/gomega v1.38.2 // indirect +) diff --git a/scripts/generate-buildpack-docs/go.sum b/scripts/generate-buildpack-docs/go.sum new file mode 100644 index 00000000..95ab58d2 --- /dev/null +++ b/scripts/generate-buildpack-docs/go.sum @@ -0,0 +1,28 @@ +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/buildpacks/libcnb/v2 v2.1.0 h1:Leq/mkEclb4nuJ6lmwZXt1H1TfhPdSXwcbR0qlp3GL4= +github.com/buildpacks/libcnb/v2 v2.1.0/go.mod h1:9VOD2kS9Knw8MuMtJOPVj/62eiUTk+Dp+lmrjM7v8Lc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/scripts/generate-buildpack-docs/main.go b/scripts/generate-buildpack-docs/main.go new file mode 100644 index 00000000..b1f85ab5 --- /dev/null +++ b/scripts/generate-buildpack-docs/main.go @@ -0,0 +1,261 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/BurntSushi/toml" + "github.com/buildpacks/libcnb/v2" +) + +// BuildpackToml represents the structure of a buildpack.toml file +type BuildpackToml struct { + API string `toml:"api"` + Buildpack libcnb.BuildpackInfo `toml:"buildpack"` + Metadata map[string]any `toml:"metadata"` + Order []libcnb.BuildpackOrder `toml:"order"` +} + +type BuildpackInfo struct { + Name string + Path string + TOML BuildpackToml + LastUpdated time.Time +} + +func main() { + // Get the project root (two levels up from scripts/generate-buildpack-docs) + projectRoot, err := filepath.Abs("../..") + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting project root: %v\n", err) + os.Exit(1) + } + + // Parse all buildpacks + buildpacks, err := parseAllBuildpacks(projectRoot) + if err != nil { + fmt.Fprintf(os.Stderr, "Error parsing buildpacks: %v\n", err) + os.Exit(1) + } + + // Sort buildpacks by name for consistent output + sort.Slice(buildpacks, func(i, j int) bool { + return buildpacks[i].Name < buildpacks[j].Name + }) + + // Create bp-docs directory structure + docsDir := filepath.Join(projectRoot, "bp-docs") + buildpacksDir := filepath.Join(docsDir, "buildpacks") + if err := os.MkdirAll(buildpacksDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating directories: %v\n", err) + os.Exit(1) + } + + // Generate index page + if err := generateIndexPage(docsDir, buildpacks); err != nil { + fmt.Fprintf(os.Stderr, "Error generating index page: %v\n", err) + os.Exit(1) + } + + // Generate individual buildpack pages + for _, bp := range buildpacks { + if err := generateBuildpackPage(buildpacksDir, bp); err != nil { + fmt.Fprintf(os.Stderr, "Error generating page for %s: %v\n", bp.Name, err) + os.Exit(1) + } + } + + fmt.Printf("Successfully generated documentation for %d buildpacks in %s\n", len(buildpacks), docsDir) +} + +func parseAllBuildpacks(projectRoot string) ([]BuildpackInfo, error) { + metaDir := filepath.Join(projectRoot, "meta") + entries, err := os.ReadDir(metaDir) + if err != nil { + return nil, fmt.Errorf("reading meta directory: %w", err) + } + + var buildpacks []BuildpackInfo + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + buildpackPath := filepath.Join(metaDir, entry.Name(), "buildpack.toml") + if _, err := os.Stat(buildpackPath); os.IsNotExist(err) { + continue + } + + var bp BuildpackToml + if _, err := toml.DecodeFile(buildpackPath, &bp); err != nil { + return nil, fmt.Errorf("parsing %s: %w", buildpackPath, err) + } + + lastUpdated, err := getLastUpdate(projectRoot, filepath.Join("meta", entry.Name(), "buildpack.toml")) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not get last update for %s: %v\n", entry.Name(), err) + lastUpdated = time.Now() + } + + buildpacks = append(buildpacks, BuildpackInfo{ + Name: entry.Name(), + Path: buildpackPath, + TOML: bp, + LastUpdated: lastUpdated, + }) + } + + return buildpacks, nil +} + +func getLastUpdate(projectRoot, relativePath string) (time.Time, error) { + cmd := exec.Command("git", "log", "-1", "--format=%ai", "--", relativePath) + cmd.Dir = projectRoot + output, err := cmd.Output() + if err != nil { + return time.Time{}, err + } + + dateStr := strings.TrimSpace(string(output)) + if dateStr == "" { + return time.Now(), nil + } + + // Parse git date format: "2025-12-19 06:11:59 +0000" + t, err := time.Parse("2006-01-02 15:04:05 -0700", dateStr) + if err != nil { + return time.Time{}, fmt.Errorf("parsing date %s: %w", dateStr, err) + } + + return t, nil +} + +// buildpackDocURLs maps buildpack directory names to their documentation URLs +var buildpackDocURLs = map[string]string{ + "dotnet-core": "/docs/recipes/deploy-dotnet/", + "go": "/docs/recipes/deploy-go/", + "java": "/docs/recipes/deploy-java/", + "java-native-image": "/docs/recipes/deploy-java/", + "nodejs": "/docs/recipes/deploy-nodejs/", + "php": "/docs/recipes/deploy-php/", + "python": "/docs/recipes/deploy-python/", + "ruby": "/docs/recipes/deploy-ruby/", + "runway": "/docs/recipes/static/", +} + +func generateIndexPage(docsDir string, buildpacks []BuildpackInfo) error { + var sb strings.Builder + + // Hugo front matter + sb.WriteString("---\n") + sb.WriteString("title: \"Runway Buildpack Stack\"\n") + sb.WriteString("type: docs\n") + sb.WriteString("weight: 1\n") + sb.WriteString("---\n\n") + + // Title and introduction + sb.WriteString("This documentation catalogs all composite buildpacks available on Runway.\n\n") + + // Table header + sb.WriteString("| Buildpack | Version | Last Updated |\n") + sb.WriteString("|-----------|---------|--------------|\n") + + // Table rows + for _, bp := range buildpacks { + name := bp.TOML.Buildpack.Name + if name == "" { + name = bp.Name + } + version := bp.TOML.Buildpack.Version + lastUpdate := bp.LastUpdated.Format("2006-01-02") + + sb.WriteString(fmt.Sprintf("| [`%s`](buildpacks/%s) | `%s` | %s |\n", + name, bp.Name, version, lastUpdate)) + } + + // Write to file + indexPath := filepath.Join(docsDir, "_index.md") + return os.WriteFile(indexPath, []byte(sb.String()), 0644) +} + +func generateBuildpackPage(buildpacksDir string, bp BuildpackInfo) error { + var sb strings.Builder + + // Look up the documentation URL for this buildpack + docURL, exists := buildpackDocURLs[bp.Name] + if !exists { + return fmt.Errorf("no documentation URL mapping found for buildpack: %s", bp.Name) + } + + // Hugo front matter + sb.WriteString("---\n") + sb.WriteString(fmt.Sprintf("title: 'Runway Docs: %s'\n", bp.TOML.Buildpack.ID)) + sb.WriteString("meta:\n") + sb.WriteString(fmt.Sprintf(" description: 'use %s to deploy on Runway'", bp.TOML.Buildpack.ID) + "\n") + if len(bp.TOML.Buildpack.Keywords) > 0 { + sb.WriteString(" keywords: runway,buildpack,") + sb.WriteString(strings.Join(bp.TOML.Buildpack.Keywords, ",") + "\n") + } + sb.WriteString("---\n") + + // Hint box with deployment guide link + sb.WriteString(fmt.Sprintf("{{}}[Learn how to use this buildpack](%s) to deploy an application on Runway.{{}}\n\n", docURL)) + + // Title and metadata + sb.WriteString(fmt.Sprintf("**ID:** `%s`\n", bp.TOML.Buildpack.ID)) + sb.WriteString(fmt.Sprintf("**Version:** `%s`\n", bp.TOML.Buildpack.Version)) + // if bp.TOML.Buildpack.Homepage != "" { + // sb.WriteString(fmt.Sprintf("**Homepage:** %s \n", bp.TOML.Buildpack.Homepage)) + // } + sb.WriteString("\n") + + // Order Groups + if len(bp.TOML.Order) > 0 { + if len(bp.TOML.Order) == 1 { + sb.WriteString("## Dependencies\n\n") + } else { + sb.WriteString("## Build Order Groups\n\n") + sb.WriteString(fmt.Sprintf("This buildpack provides %d different build configurations.\n\n", len(bp.TOML.Order))) + } + + for idx, order := range bp.TOML.Order { + if len(bp.TOML.Order) > 1 { + sb.WriteString(fmt.Sprintf("### Order %d\n\n", idx+1)) + } + + // Sort dependencies: required first, then optional, alphabetically within each group + groups := order.Groups + sort.Slice(groups, func(i, j int) bool { + if groups[i].Optional != groups[j].Optional { + return !groups[i].Optional + } + return groups[i].ID < groups[j].ID + }) + + sb.WriteString("| Dependency | Version | Required |\n") + sb.WriteString("|------------|---------|----------|\n") + + for _, group := range groups { + required := "Yes" + if group.Optional { + required = "Optional" + } + sb.WriteString(fmt.Sprintf("| `%s` | `%s` | %s |\n", group.ID, group.Version, required)) + } + + sb.WriteString("\n") + } + } + + // Last updated + sb.WriteString(fmt.Sprintf("**Last Updated:** %s\n", bp.LastUpdated.Format("2006-01-02"))) + + // Write to file + pagePath := filepath.Join(buildpacksDir, bp.Name+".md") + return os.WriteFile(pagePath, []byte(sb.String()), 0644) +}