diff --git a/.github/workflows/go-module-bump-template.yaml b/.github/workflows/go-module-bump-template.yaml new file mode 100644 index 000000000..1c6315327 --- /dev/null +++ b/.github/workflows/go-module-bump-template.yaml @@ -0,0 +1,79 @@ +name: Go Module bump (template) + +on: + workflow_dispatch: # Manual workflow trigger + inputs: + go-module: + description: "Go module to bump" + required: true + version: + description: "Go module version" + default: "latest" + required: true + +defaults: + run: + shell: bash + +jobs: + go-mod-bump: + name: Go Module bump (template) + runs-on: ubuntu-latest + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + include: [] + + steps: + - name: Configure Git user + run: | + git config --global user.email "serverless-support@redhat.com" + git config --global user.name "serverless-qe" + + - name: Checkout repo + uses: actions/checkout@v4 + with: + repository: openshift-knative/${{ matrix.repo }} + ref: ${{ matrix.branch }} + + - name: Check if ${{ inputs.go-module }} is in go.mod + id: gomod-check + run: | + if grep -iP "\t${{ inputs.go-module }} " go.mod; then + echo "${{ inputs.go-module }} found in projects go.mod" + echo "module-exists=true" >> "$GITHUB_OUTPUT" + else + echo "${{ inputs.go-module }} not found in projects go.mod" + echo "module-exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Bump Go module + if: steps.gomod-check.outputs.module-exists == 'true' + run: | + go get ${{ inputs.go-module }}@${{ inputs.version }} + + if [[ -n "${{ matrix.postUpdateCmd }}" ]]; then + ${{ matrix.postUpdateCmd }} + fi + + - name: Create Pull Request + if: steps.gomod-check.outputs.module-exists == 'true' + env: + GH_TOKEN: ${{ secrets.SERVERLESS_QE_ROBOT }} + GITHUB_TOKEN: ${{ secrets.SERVERLESS_QE_ROBOT }} + run: | + set -x + git remote add fork "https://github.com/serverless-qe/hack.git" + branch="$(echo "bump-${{ inputs.go-module }}-${{ inputs.version }}-${{ matrix.branch }}" | tr '[:upper:]' '[:lower:]')" + remote_exists=$(git ls-remote --heads fork "$branch") + if [ -z "$remote_exists" ]; then + # remote doesn't exist. + git push "https://serverless-qe:${GH_TOKEN}@github.com/serverless-qe/${{ matrix.repo }}.git" "$branch:$branch" -f || exit 1 + fi + git fetch fork "$branch" + if git diff --quiet "fork/$branch" "$branch"; then + echo "Branches are identical. No need to force push." + else + git push "https://serverless-qe:${GH_TOKEN}@github.com/serverless-qe/${{ matrix.repo }}.git" "$branch:$branch" -f + fi + gh pr create --base main --head "serverless-qe:$branch" --title "[${{ matrix.branch }}] Bump ${{ inputs.go-module }} to ${{ inputs.version }}" --body "Bumping ${{ inputs.go-module }} to ${{ inputs.version }}" || true diff --git a/.github/workflows/go-module-bump.yaml b/.github/workflows/go-module-bump.yaml new file mode 100644 index 000000000..eb75bfe66 --- /dev/null +++ b/.github/workflows/go-module-bump.yaml @@ -0,0 +1,183 @@ +name: Go Module bump +on: + workflow_dispatch: # Manual workflow trigger + inputs: + go-module: + description: "Go module to bump" + required: true + version: + description: "Go module version" + default: "latest" + required: true +defaults: + run: + shell: bash +jobs: + go-mod-bump: + name: Go Module bump + runs-on: ubuntu-latest + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + include: + - repo: backstage-plugins + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: backstage-plugins + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: backstage-plugins + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: backstage-plugins + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: client + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: client + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: eventing-hyperfoil-benchmark + branch: main + postUpdateCmd: make generate-release + - repo: eventing-istio + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: eventing-istio + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: eventing-istio + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: eventing-istio + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: eventing-kafka-broker + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: eventing-kafka-broker + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: eventing-kafka-broker + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: eventing-kafka-broker + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: eventing + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: eventing + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: eventing + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: eventing + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: kn-plugin-event + branch: release-1.15 + - repo: kn-plugin-event + branch: release-1.16 + - repo: kn-plugin-func + branch: release-v1.15 + - repo: kn-plugin-func + branch: release-v1.16 + - repo: kn-plugin-func + branch: serverless-1.34 + - repo: serverless-operator + branch: main + postUpdateCmd: make generated-files + - repo: serverless-operator + branch: release-1.35 + postUpdateCmd: make generated-files + - repo: serverless-operator + branch: release-1.36 + postUpdateCmd: make generated-files + - repo: net-istio + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: net-istio + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: net-istio + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: net-istio + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: net-kourier + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: net-kourier + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: net-kourier + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: net-kourier + branch: release-v1.16 + postUpdateCmd: make generate-release + - repo: serving + branch: release-v1.12 + postUpdateCmd: make generate-release + - repo: serving + branch: release-v1.14 + postUpdateCmd: make generate-release + - repo: serving + branch: release-v1.15 + postUpdateCmd: make generate-release + - repo: serving + branch: release-v1.16 + postUpdateCmd: make generate-release + steps: + - name: Configure Git user + run: | + git config --global user.email "serverless-support@redhat.com" + git config --global user.name "serverless-qe" + - name: Checkout repo + uses: actions/checkout@v4 + with: + repository: openshift-knative/${{ matrix.repo }} + ref: ${{ matrix.branch }} + - name: Check if ${{ inputs.go-module }} is in go.mod + id: gomod-check + run: | + if grep -iP "\t${{ inputs.go-module }} " go.mod; then + echo "${{ inputs.go-module }} found in projects go.mod" + echo "module-exists=true" >> "$GITHUB_OUTPUT" + else + echo "${{ inputs.go-module }} not found in projects go.mod" + echo "module-exists=false" >> "$GITHUB_OUTPUT" + fi + - name: Bump Go module + if: steps.gomod-check.outputs.module-exists == 'true' + run: | + go get ${{ inputs.go-module }}@${{ inputs.version }} + + if [[ -n "${{ matrix.postUpdateCmd }}" ]]; then + ${{ matrix.postUpdateCmd }} + fi + - name: Create Pull Request + if: steps.gomod-check.outputs.module-exists == 'true' + env: + GH_TOKEN: ${{ secrets.SERVERLESS_QE_ROBOT }} + GITHUB_TOKEN: ${{ secrets.SERVERLESS_QE_ROBOT }} + run: | + set -x + git remote add fork "https://github.com/serverless-qe/hack.git" + branch="$(echo "bump-${{ inputs.go-module }}-${{ inputs.version }}-${{ matrix.branch }}" | tr '[:upper:]' '[:lower:]')" + remote_exists=$(git ls-remote --heads fork "$branch") + if [ -z "$remote_exists" ]; then + # remote doesn't exist. + git push "https://serverless-qe:${GH_TOKEN}@github.com/serverless-qe/${{ matrix.repo }}.git" "$branch:$branch" -f || exit 1 + fi + git fetch fork "$branch" + if git diff --quiet "fork/$branch" "$branch"; then + echo "Branches are identical. No need to force push." + else + git push "https://serverless-qe:${GH_TOKEN}@github.com/serverless-qe/${{ matrix.repo }}.git" "$branch:$branch" -f + fi + gh pr create --base main --head "serverless-qe:$branch" --title "[${{ matrix.branch }}] Bump ${{ inputs.go-module }} to ${{ inputs.version }}" --body "Bumping ${{ inputs.go-module }} to ${{ inputs.version }}" || true diff --git a/Makefile b/Makefile index 1464f992a..691780724 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ unit-tests: rm -rf openshift/project/.github mkdir -p openshift - go run ./cmd/generate-ci-action --input ".github/workflows/release-generate-ci-template.yaml" --config "config/" --output "openshift/release-generate-ci.yaml" + go run ./cmd/generate-ci-action --config "config/" --output "openshift/" # If the following fails, please run 'make generate-ci-action' diff -r "openshift/release-generate-ci.yaml" ".github/workflows/release-generate-ci.yaml" diff --git a/cmd/generate-ci-action/main.go b/cmd/generate-ci-action/main.go index cc68db2f2..04c2f907e 100644 --- a/cmd/generate-ci-action/main.go +++ b/cmd/generate-ci-action/main.go @@ -16,15 +16,10 @@ func main() { defer cancel() inputConfig := flag.String("config", filepath.Join("config"), "Specify repositories config") - inputAction := flag.String("input", filepath.Join(".github", "workflows", "release-generate-ci-template.yaml"), "Input action (template)") - outputAction := flag.String("output", filepath.Join(".github", "workflows", "release-generate-ci.yaml"), "Output action") + outputFolder := flag.String("output", filepath.Join(".github", "workflows"), "Output folder for the actions") flag.Parse() - err := action.UpdateAction(ctx, action.Config{ - InputAction: *inputAction, - InputConfigPath: *inputConfig, - OutputAction: *outputAction, - }) + err := action.Generate(ctx, *inputConfig, *outputFolder) if err != nil { log.Fatal(err) } diff --git a/pkg/action/action.go b/pkg/action/action.go new file mode 100644 index 000000000..4c0c40efd --- /dev/null +++ b/pkg/action/action.go @@ -0,0 +1,96 @@ +package action + +import ( + "bytes" + "cmp" + "context" + "fmt" + "path/filepath" + "sort" + + "gopkg.in/yaml.v3" +) + +type Config struct { + InputAction string + InputConfigPath string + OutputAction string +} + +func Generate(ctx context.Context, inputConfig string, outputFolder string) error { + if err := UpdateAction(ctx, Config{ + InputConfigPath: inputConfig, + InputAction: ".github/workflows/release-generate-ci-template.yaml", + OutputAction: filepath.Join(outputFolder, "release-generate-ci.yaml"), + }); err != nil { + return fmt.Errorf("could not generate update action: %w", err) + } + + if err := GoModuleBumpAction(ctx, Config{ + InputConfigPath: inputConfig, + InputAction: ".github/workflows/go-module-bump-template.yaml", + OutputAction: filepath.Join(outputFolder, "go-module-bump.yaml"), + }); err != nil { + return fmt.Errorf("could not generate Go module bump action: %w", err) + } + + return nil +} + +func AddNestedField(node *yaml.Node, value interface{}, prepend bool, fields ...string) error { + + for i, n := range node.Content { + + if i > 0 && node.Content[i-1].Value == fields[0] { + + // Base case for scalar nodes + if len(fields) == 1 && n.Kind == yaml.ScalarNode { + n.SetString(fmt.Sprintf("%s", value)) + break + } + // base case for sequence node + if len(fields) == 1 && n.Kind == yaml.SequenceNode { + + if v, ok := value.([]interface{}); ok { + var s yaml.Node + + b, err := yaml.Marshal(v) + if err != nil { + return err + } + if err := yaml.NewDecoder(bytes.NewBuffer(b)).Decode(&s); err != nil { + return err + } + + if prepend { + n.Content = append(s.Content[0].Content, n.Content...) + } else { + n.Content = append(n.Content, s.Content[0].Content...) + } + + // print list entries in a single line each + n.Style = yaml.LiteralStyle + } + break + } + + // Continue to the next level + return AddNestedField(n, value, prepend, fields[1:]...) + } + + if node.Kind == yaml.DocumentNode { + return AddNestedField(n, value, prepend, fields...) + } + } + + return nil +} + +func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K { + keys := make([]K, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) + return keys +} diff --git a/pkg/action/go_mod_bump_action.go b/pkg/action/go_mod_bump_action.go new file mode 100644 index 000000000..9850181f4 --- /dev/null +++ b/pkg/action/go_mod_bump_action.go @@ -0,0 +1,93 @@ +package action + +import ( + "bytes" + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/openshift-knative/hack/pkg/prowgen" +) + +type BumpRepoConfig struct { + Repo string `yaml:"repo"` + Branch string `yaml:"branch"` + PostUpdateCmd string `yaml:"postUpdateCmd,omitempty"` +} + +func GoModuleBumpAction(ctx context.Context, cfg Config) error { + y, err := os.ReadFile(cfg.InputAction) + if err != nil { + return err + } + var node yaml.Node + if err := yaml.NewDecoder(bytes.NewBuffer(y)).Decode(&node); err != nil { + return fmt.Errorf("failed to decode file into node: %w", err) + } + + if err := AddNestedField(&node, "Go Module bump", false, "name"); err != nil { + return fmt.Errorf("failed to rename workflow: %w", err) + } + + if err := AddNestedField(&node, "Go Module bump", false, "jobs", "go-mod-bump", "name"); err != nil { + return fmt.Errorf("failed to rename workflow: %w", err) + } + + var repoConfigs []interface{} + err = filepath.Walk(cfg.InputConfigPath, func(path string, info fs.FileInfo, err error) error { + if info.IsDir() || !strings.HasSuffix(path, ".yaml") { + return nil + } + + inConfig, err := prowgen.LoadConfig(path) + if err != nil { + return err + } + + for _, repo := range inConfig.Repositories { + sortedBranches := sortedKeys(inConfig.Config.Branches) + for _, branchName := range sortedBranches { + + if branchName == "release-next" { + continue + } + + repoConfig := BumpRepoConfig{ + Repo: repo.Repo, + Branch: branchName, + PostUpdateCmd: repo.RunCodegenCommand(), + } + + repoConfigs = append(repoConfigs, repoConfig) + } + } + + return nil + }) + if err != nil { + return fmt.Errorf("failed to walk filesystem path %q: %w", cfg.InputConfigPath, err) + } + + if err := AddNestedField(&node, repoConfigs, false, "jobs", "go-mod-bump", "strategy", "matrix", "include"); err != nil { + return fmt.Errorf("failed to add repo config as matrix entry: %w", err) + } + + buf := bytes.NewBuffer(nil) + enc := yaml.NewEncoder(buf) + enc.SetIndent(2) + if err := enc.Encode(&node); err != nil { + return fmt.Errorf("failed to encode node into buf: %w", err) + } + defer enc.Close() + + if err := os.WriteFile(cfg.OutputAction, buf.Bytes(), 0600); err != nil { + return fmt.Errorf("failed to write updates: %w", err) + } + + return nil +} diff --git a/pkg/action/update_action.go b/pkg/action/update_action.go index 16924df64..f5c7ab3d5 100644 --- a/pkg/action/update_action.go +++ b/pkg/action/update_action.go @@ -2,14 +2,12 @@ package action import ( "bytes" - "cmp" "context" "fmt" "io/fs" "log" "os" "path/filepath" - "sort" "strings" "sync" @@ -19,12 +17,6 @@ import ( "github.com/openshift-knative/hack/pkg/prowgen" ) -type Config struct { - InputAction string - InputConfigPath string - OutputAction string -} - func UpdateAction(ctx context.Context, cfg Config) error { var steps []interface{} var cloneSteps []interface{} @@ -226,58 +218,3 @@ gh pr create --base "$target_branch" --head "serverless-qe:$branch" --title "[$t } return cloneSteps, steps, nil } - -func AddNestedField(node *yaml.Node, value interface{}, prepend bool, fields ...string) error { - - for i, n := range node.Content { - - if i > 0 && node.Content[i-1].Value == fields[0] { - - // Base case for scalar nodes - if len(fields) == 1 && n.Kind == yaml.ScalarNode { - n.SetString(fmt.Sprintf("%s", value)) - break - } - // base case for sequence node - if len(fields) == 1 && n.Kind == yaml.SequenceNode { - - if v, ok := value.([]interface{}); ok { - var s yaml.Node - - b, err := yaml.Marshal(v) - if err != nil { - return err - } - if err := yaml.NewDecoder(bytes.NewBuffer(b)).Decode(&s); err != nil { - return err - } - - if prepend { - n.Content = append(s.Content[0].Content, n.Content...) - } else { - n.Content = append(n.Content, s.Content[0].Content...) - } - } - break - } - - // Continue to the next level - return AddNestedField(n, value, prepend, fields[1:]...) - } - - if node.Kind == yaml.DocumentNode { - return AddNestedField(n, value, prepend, fields...) - } - } - - return nil -} - -func sortedKeys[K cmp.Ordered, V any](m map[K]V) []K { - keys := make([]K, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) - return keys -} diff --git a/pkg/discover/discover.go b/pkg/discover/discover.go index 763e1c69b..c7d2e8be3 100644 --- a/pkg/discover/discover.go +++ b/pkg/discover/discover.go @@ -28,8 +28,7 @@ func Main() { defer cancel() inputConfig := flag.String("config", filepath.Join("config"), "Specify repositories config") - inputAction := flag.String("input", filepath.Join(".github", "workflows", "release-generate-ci-template.yaml"), "Input action (template)") - outputAction := flag.String("output", filepath.Join(".github", "workflows", "release-generate-ci.yaml"), "Output action") + outputFolder := flag.String("output", filepath.Join(".github", "workflows"), "Output folder for the actions") flag.Parse() err := filepath.Walk(*inputConfig, func(path string, info fs.FileInfo, err error) error { @@ -46,12 +45,9 @@ func Main() { if err != nil { log.Fatalln("Failed to walk path", *inputConfig, err) } + flag.Parse() - err = action.UpdateAction(ctx, action.Config{ - InputAction: *inputAction, - InputConfigPath: *inputConfig, - OutputAction: *outputAction, - }) + err = action.Generate(ctx, *inputConfig, *outputFolder) if err != nil { log.Fatal(err) }