├── .cmdx.yaml ├── .github └── workflows │ ├── actionlint.yaml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── README.md ├── aqua-checksums.json ├── aqua.yaml ├── aqua ├── actionlint.yaml ├── cmdx.yaml ├── cosign.yaml ├── ghalint.yaml ├── ghcp.yaml ├── golangci-lint.yaml ├── goreleaser.yaml └── reviewdog.yaml ├── ci └── test.sh ├── cmd └── circleci-config-merge │ └── main.go ├── examples ├── .circleci │ └── config.yml ├── README.md ├── bar │ └── .circleci │ │ └── config.yml ├── foo │ └── .circleci │ │ └── config.yml ├── generate.sh ├── header.txt └── merge.sh ├── githooks └── pre-commit.sh ├── go.mod ├── go.sum ├── pkg ├── cli │ ├── run.go │ └── runner.go └── controller │ ├── config.go │ ├── config_internal_test.go │ ├── config_test.go │ ├── controller.go │ ├── job.go │ ├── job_internal_test.go │ ├── map.go │ ├── map_internal_test.go │ ├── new.go │ ├── workflow.go │ ├── workflow_test.go │ ├── workflows.go │ ├── workflows_internal_test.go │ └── workflows_test.go ├── renovate.json5 └── scripts ├── coverage.sh ├── fmt.sh ├── githook.sh └── test-code-climate.sh /.cmdx.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # the configuration file of cmdx - task runner 3 | # https://github.com/suzuki-shunsuke/cmdx 4 | tasks: 5 | - name: init 6 | short: i 7 | script: bash scripts/githook.sh 8 | description: setup git hooks 9 | usage: setup git hooks 10 | - name: coverage 11 | short: c 12 | description: test a package 13 | usage: test a package 14 | script: "bash scripts/coverage.sh {{.path}}" 15 | args: 16 | - name: path 17 | - name: test 18 | short: t 19 | description: test 20 | usage: test 21 | script: go test -v ./... -race -covermode=atomic 22 | - name: fmt 23 | description: format the go code 24 | usage: format the go code 25 | script: bash scripts/fmt.sh 26 | - name: vet 27 | short: v 28 | description: go vet 29 | usage: go vet 30 | script: go vet ./... 31 | - name: lint 32 | short: l 33 | description: lint the go code 34 | usage: lint the go code 35 | script: golangci-lint run 36 | - name: release 37 | short: r 38 | description: release the new version 39 | usage: release the new version 40 | script: | 41 | git tag -m "chore: release {{.version}}" "{{.version}}" 42 | git push origin "{{.version}}" 43 | args: 44 | - name: version 45 | required: true 46 | validate: 47 | - regexp: "^v\\d+\\.\\d+.\\d+(-\\d+)?$" 48 | - name: build 49 | short: b 50 | description: build 51 | usage: binary 52 | script: "go build -o dist/circleci-config-merge ./cmd/circleci-config-merge" 53 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Separate the workflow for actionlint to other workflows, because if a workflow for actionlint is broken actionlint isn't run 3 | name: actionlint 4 | on: 5 | pull_request: 6 | paths: 7 | - .github/workflows/*.yaml 8 | - aqua/actionlint.yaml 9 | - aqua/reviewdog.yaml 10 | jobs: 11 | actionlint: 12 | uses: suzuki-shunsuke/actionlint-workflow/.github/workflows/actionlint.yaml@f39bb91c0f9391bea9750f89252fb364f9d64c13 # v1.2.0 13 | with: 14 | aqua_version: v2.38.0 15 | permissions: 16 | pull-requests: write 17 | contents: read 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | tags: [v*] 6 | jobs: 7 | release: 8 | uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@d98e23ec5255820653b80250e15e1eb160537908 # v1.1.0 9 | with: 10 | homebrew: true 11 | go-version: 1.23.3 12 | aqua_version: v2.38.0 13 | secrets: 14 | gh_app_id: ${{secrets.APP_ID}} 15 | gh_app_private_key: ${{secrets.APP_PRIVATE_KEY}} 16 | winget_github_token: ${{secrets.WINGET_ACCESS_TOKEN}} 17 | permissions: 18 | contents: write 19 | id-token: write 20 | actions: read 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | 4 | on: pull_request 5 | 6 | env: 7 | AQUA_POLICY_CONFIG: ${{ github.workspace }}/aqua-policy.yaml 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | test: 13 | uses: suzuki-shunsuke/go-test-full-workflow/.github/workflows/test.yaml@c37f3fa8a1dc979f7c4152ea0850eef1cbad7c2f # v1.1.1 14 | with: 15 | aqua_version: v2.38.0 16 | go-version: 1.23.3 17 | secrets: 18 | gh_app_id: ${{secrets.APP_ID}} 19 | gh_app_private_key: ${{secrets.APP_PRIVATE_KEY}} 20 | permissions: 21 | pull-requests: write 22 | contents: read # To checkout private repository 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .code-climate 3 | .git-rm-branch.yml 4 | # install cc-test-reporter in CI 5 | bin/* 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | enable-all: true 4 | disable: 5 | - scopelint # WARN [runner] The linter 'scopelint' is deprecated (since v1.39.0) due to: The repository of the linter has been deprecated by the owner. Replaced by exportloopref. 6 | - maligned # WARN [runner] The linter 'maligned' is deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner. Replaced by govet 'fieldalignment'. 7 | - interfacer # WARN [runner] The linter 'interfacer' is deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner. 8 | - varcheck # WARN The linter 'varcheck' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. 9 | - ifshort # WARN The linter 'ifshort' is deprecated (since v1.48.0) due to: The repository of the linter has been deprecated by the owner. 10 | - structcheck # WARN The linter 'structcheck' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. 11 | - nosnakecase # WARN The linter 'nosnakecase' is deprecated (since v1.48.1) due to: The repository of the linter has been deprecated by the owner. Replaced by revive 'var-naming'. 12 | - deadcode # WARN The linter 'deadcode' is deprecated (since v1.49.0) due to: The owner seems to have abandoned the linter. Replaced by unused. 13 | - wsl 14 | - goerr113 15 | - lll 16 | - godot 17 | - nlreturn 18 | - godox 19 | - golint 20 | - exhaustivestruct # https://github.com/mbilski/exhaustivestruct 21 | - exhaustruct 22 | - varnamelen 23 | - tagliatelle 24 | - depguard 25 | - musttag 26 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | builds: 4 | - binary: circleci-config-merge 5 | main: cmd/circleci-config-merge/main.go 6 | env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - windows 10 | - darwin 11 | - linux 12 | goarch: 13 | - amd64 14 | - arm64 15 | release: 16 | prerelease: true 17 | archives: 18 | - format_overrides: 19 | - goos: windows 20 | format: zip 21 | brews: 22 | - 23 | # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the 24 | # same kind. We will probably unify this in the next major version like it is done with scoop. 25 | 26 | # GitHub/GitLab repository to push the formula to 27 | repository: 28 | owner: suzuki-shunsuke 29 | name: homebrew-circleci-config-merge 30 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 31 | # The project name and current git tag are used in the format string. 32 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 33 | # Your app's homepage. 34 | # Default is empty. 35 | homepage: https://github.com/suzuki-shunsuke/circleci-config-merge 36 | 37 | # Template of your app's description. 38 | # Default is empty. 39 | description: Generate .circleci/config.yml by merging multiple files 40 | license: MIT 41 | 42 | # Setting this will prevent goreleaser to actually try to commit the updated 43 | # formula - instead, the formula file will be stored on the dist folder only, 44 | # leaving the responsibility of publishing it to the user. 45 | # If set to auto, the release will not be uploaded to the homebrew tap 46 | # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 47 | # Default is false. 48 | skip_upload: auto 49 | 50 | # So you can `brew test` your formula. 51 | # Default is empty. 52 | test: | 53 | system "#{bin}/circleci-config-merge --version" 54 | 55 | signs: 56 | - cmd: cosign 57 | artifacts: checksum 58 | signature: ${artifact}.sig 59 | certificate: ${artifact}.pem 60 | output: true 61 | args: 62 | - sign-blob 63 | - "-y" 64 | - --output-signature 65 | - ${signature} 66 | - --output-certificate 67 | - ${certificate} 68 | - --oidc-provider 69 | - github 70 | - ${artifact} 71 | 72 | winget: 73 | - publisher: suzuki-shunsuke 74 | short_description: Generate .circleci/config.yml by merging multiple files 75 | license: mit 76 | 77 | # Publisher URL. 78 | # 79 | # Templates: allowed 80 | # publisher_url: https://goreleaser.com 81 | 82 | publisher_support_url: https://github.com/suzuki-shunsuke/circleci-config-merge/issues 83 | 84 | # GOAMD64 to specify which amd64 version to use if there are multiple 85 | # versions from the build section. 86 | # 87 | # Default: v1 88 | # goamd64: v1 89 | 90 | # URL which is determined by the given Token (github, gitlab or gitea). 91 | # 92 | # Default depends on the client. 93 | # Templates: allowed 94 | url_template: "https://github.com/suzuki-shunsuke/circleci-config-merge/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 95 | 96 | # Git author used to commit to the repository. 97 | # commit_author: 98 | # name: Shunsuke Suzuki 99 | # email: suzuki.shunsuke.1989@gmail.com 100 | 101 | # The project name and current git tag are used in the format string. 102 | # 103 | # Templates: allowed 104 | # commit_msg_template: "{{ .PackageIdentifier }}: {{ .Tag }}" 105 | 106 | # Path for the file inside the repository. 107 | # 108 | # Default: manifests/// 109 | # path: manifests/g/goreleaser/1.19 110 | 111 | homepage: https://github.com/suzuki-shunsuke/circleci-config-merge 112 | 113 | # Your app's long description. 114 | # 115 | # Templates: allowed 116 | description: Generate .circleci/config.yml by merging multiple files. 117 | 118 | # License URL. 119 | # 120 | # Templates: allowed 121 | license_url: https://github.com/suzuki-shunsuke/circleci-config-merge/blob/main/LICENSE 122 | 123 | # Copyright. 124 | # 125 | # Templates: allowed 126 | # copyright: "" 127 | 128 | # Copyright URL. 129 | # 130 | # Templates: allowed 131 | # copyright_url: "" 132 | 133 | # Setting this will prevent goreleaser to actually try to commit the updated 134 | # package - instead, it will be stored on the dist folder only, 135 | # leaving the responsibility of publishing it to the user. 136 | # 137 | # If set to auto, the release will not be uploaded to the repository 138 | # in case there is an indicator for prerelease in the tag e.g. v1.0.0-rc1 139 | # 140 | # Templates: allowed 141 | skip_upload: "auto" 142 | 143 | # Release notes. 144 | # 145 | # If you want to use the release notes generated by GoReleaser, use 146 | # `{{.Changelog}}` as the value. 147 | # 148 | # Templates: allowed 149 | # release_notes: "{{.Changelog}}" 150 | 151 | # Release notes URL. 152 | # 153 | # Templates: allowed 154 | release_notes_url: https://github.com/suzuki-shunsuke/circleci-config-merge/releases/tag/{{.Tag}} 155 | 156 | # Tags. 157 | tags: 158 | - circleci 159 | 160 | # Repository to push the generated files to. 161 | repository: 162 | owner: suzuki-shunsuke 163 | # owner: mini-core 164 | name: winget-pkgs 165 | 166 | token: "{{ .Env.WINGET_GITHUB_TOKEN }}" 167 | 168 | # Optionally a branch can be provided. 169 | # 170 | # Default: default repository branch 171 | # Templates: allowed 172 | branch: "circleci-config-merge-{{.Version}}" 173 | 174 | # Sets up pull request creation instead of just pushing to the given branch. 175 | # Make sure the 'branch' property is different from base before enabling 176 | # it. 177 | # 178 | # Since: v1.17 179 | pull_request: 180 | # Whether to enable it or not. 181 | enabled: true 182 | 183 | # Whether to open the PR as a draft or not. 184 | # 185 | # Since: v1.19 186 | draft: true 187 | 188 | # Base can also be another repository, in which case the owner and name 189 | # above will be used as HEAD, allowing cross-repository pull requests. 190 | # 191 | # Since: v1.19 192 | base: 193 | owner: microsoft 194 | # owner: mini-core 195 | name: winget-pkgs 196 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shunsuke Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circleci-config-merge 2 | 3 | [![Build Status](https://github.com/suzuki-shunsuke/circleci-config-merge/workflows/CI/badge.svg)](https://github.com/suzuki-shunsuke/circleci-config-merge/actions) 4 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b34ffd9a1198b2952d46/test_coverage)](https://codeclimate.com/github/suzuki-shunsuke/circleci-config-merge/test_coverage) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/suzuki-shunsuke/circleci-config-merge)](https://goreportcard.com/report/github.com/suzuki-shunsuke/circleci-config-merge) 6 | [![GitHub last commit](https://img.shields.io/github/last-commit/suzuki-shunsuke/circleci-config-merge.svg)](https://github.com/suzuki-shunsuke/circleci-config-merge) 7 | [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/suzuki-shunsuke/circleci-config-merge/main/LICENSE) 8 | 9 | Generate .circleci/config.yml by merging multiple files 10 | 11 | ## Blog 12 | 13 | https://dev.to/suzukishunsuke/splitting-circleci-config-yml-10gk 14 | 15 | ## Motivation 16 | 17 | Our motivation is to split a huge .circleci/config.yml per service. 18 | We have a [monorepo](https://en.wikipedia.org/wiki/Monorepo) where many services are managed. 19 | `.circleci/config.yml` of this repository has over 6000 lines, and it's hard to maintain the file. 20 | 21 | ## Why don't we use `circleci config pack`? 22 | 23 | The directory structure and naming rule don't match our needs. 24 | And we want to merge the list of workflow's jobs. 25 | 26 | ## Install 27 | 28 | Download from [GitHub Releases](https://github.com/suzuki-shunsuke/circleci-config-merge/releases). 29 | You can install circleci-config-merge with [Homebrew](https://brew.sh/) too. 30 | 31 | ```console 32 | $ brew install suzuki-shunsuke/circleci-config-merge/circleci-config-merge 33 | ``` 34 | 35 | You can install circleci-config-merge with [aqua](https://aquaproj.github.io/) too. 36 | 37 | After installing circleci-config-merge, please check if it is installed properly. 38 | 39 | ``` 40 | $ circleci-config-merge --version 41 | circleci-config-merge version 0.1.0 42 | ``` 43 | 44 | ## Example 45 | 46 | Please see [examples](examples) and [example-cirleci-config-merge](https://github.com/suzuki-shunsuke/example-circleci-config-merge). 47 | 48 | ## How to use 49 | 50 | ``` 51 | $ circleci-config-merge merge [ ...] > .circleci/config.yml 52 | ``` 53 | 54 | ## How to test in CI 55 | 56 | In CI, we should test whether `.circleci/config.yml` and the result of `circleci-config-merge merge` is equal as YAML. 57 | `circleci-config-merge` doesn't provide the feature to compare YAML, so please use the other tool like [dyff](https://github.com/homeport/dyff). 58 | Please see the example [suzuki-shunsuke/example-circleci-config-merge](https://github.com/suzuki-shunsuke/example-circleci-config-merge) as a reference to split .circleci/config.yml and setup CI. 59 | 60 | ## Split File Format 61 | 62 | The split file format is same as [.circleci/config.yml](https://circleci.com/docs/2.0/configuration-reference/). 63 | 64 | ## Merge Rule 65 | 66 | Coming soon. 67 | 68 | ## LICENSE 69 | 70 | [MIT](LICENSE) 71 | -------------------------------------------------------------------------------- /aqua-checksums.json: -------------------------------------------------------------------------------- 1 | { 2 | "checksums": [ 3 | { 4 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-darwin-amd64.tar.gz", 5 | "checksum": "0ED6F1A216DDB62E293858196799608D63894BD2EC178114484363CA45CDE84B", 6 | "algorithm": "sha256" 7 | }, 8 | { 9 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-darwin-arm64.tar.gz", 10 | "checksum": "DDE51958F0F24D442062B5709B6912D91E235115DFE5887E80B3E5602C9CC09B", 11 | "algorithm": "sha256" 12 | }, 13 | { 14 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-linux-amd64.tar.gz", 15 | "checksum": "53695531EEB824B6883C703335CEF6F07882F8BA6FEDC00ED43853EA07FA1FBD", 16 | "algorithm": "sha256" 17 | }, 18 | { 19 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-linux-arm64.tar.gz", 20 | "checksum": "E1E47209D7BDD288FD8CFE88548B477DF2F7ECA81B0E9EC1F9D45604F79185EB", 21 | "algorithm": "sha256" 22 | }, 23 | { 24 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-windows-amd64.zip", 25 | "checksum": "34E980AFE44655C395AA65F96953FC4B6A2E58206F1A7370AB88407B187184C8", 26 | "algorithm": "sha256" 27 | }, 28 | { 29 | "id": "github_release/github.com/golangci/golangci-lint/v1.62.0/golangci-lint-1.62.0-windows-arm64.zip", 30 | "checksum": "6CBE5C705F098B4AF79DAF3359C4C92BEA847215EA8D023E90436BC3A671842D", 31 | "algorithm": "sha256" 32 | }, 33 | { 34 | "id": "github_release/github.com/goreleaser/goreleaser/v2.4.8/goreleaser_Darwin_all.tar.gz", 35 | "checksum": "AF78A0E0AE01E9687B23B18CBED7CFDC9A15F89092789AA9CE3EB021499959EE", 36 | "algorithm": "sha256" 37 | }, 38 | { 39 | "id": "github_release/github.com/goreleaser/goreleaser/v2.4.8/goreleaser_Linux_arm64.tar.gz", 40 | "checksum": "B04032A54E40FC80EB6D8A3A7B428AB5CB3DD49606032D1AB14200D7F8287BE9", 41 | "algorithm": "sha256" 42 | }, 43 | { 44 | "id": "github_release/github.com/goreleaser/goreleaser/v2.4.8/goreleaser_Linux_x86_64.tar.gz", 45 | "checksum": "A115C78EDC90D0EB5D36272C54A8087C0B209644349F3E720E2EC53D48D77647", 46 | "algorithm": "sha256" 47 | }, 48 | { 49 | "id": "github_release/github.com/goreleaser/goreleaser/v2.4.8/goreleaser_Windows_arm64.zip", 50 | "checksum": "9F33640E74910CF38ED2FCE591439422D63A4C419FD0FB7A148D38A642E893AF", 51 | "algorithm": "sha256" 52 | }, 53 | { 54 | "id": "github_release/github.com/goreleaser/goreleaser/v2.4.8/goreleaser_Windows_x86_64.zip", 55 | "checksum": "D741624E79ADFD927A9D8B1D5137FA0E930826AF3E01D9AA6DE40983428F4E20", 56 | "algorithm": "sha256" 57 | }, 58 | { 59 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_darwin_amd64.zip", 60 | "checksum": "6728BD668888A64C71BF01D9AFBA373F38D353B79D181B1401A4E5E4B329289D", 61 | "algorithm": "sha256" 62 | }, 63 | { 64 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_linux_amd64.zip", 65 | "checksum": "3C808E566F0B663182AD5B5DD6E6B05DC8346610EA5613EA8C22AB19F47A4493", 66 | "algorithm": "sha256" 67 | }, 68 | { 69 | "id": "github_release/github.com/int128/ghcp/v1.13.5/ghcp_windows_amd64.zip", 70 | "checksum": "1A90AD7927F720EB8996AE143DF5D7AA2DF70689B7B5B9074D6FD32C244D688E", 71 | "algorithm": "sha256" 72 | }, 73 | { 74 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Darwin_arm64.tar.gz", 75 | "checksum": "595D340888463796B4585FF5D1F2B0B9084E20E9A76692B4271EC92C6F4D6C64", 76 | "algorithm": "sha256" 77 | }, 78 | { 79 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Darwin_x86_64.tar.gz", 80 | "checksum": "F4FE03E62D81FDBDA24F170DA9A36690EFE6B58B534B37CA37A848E1A2C1AD1C", 81 | "algorithm": "sha256" 82 | }, 83 | { 84 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Linux_arm64.tar.gz", 85 | "checksum": "7142B52EA8734CA821AB82C36B0094D6B4B9F2B8A2ADB223CFD708CEB5581BE6", 86 | "algorithm": "sha256" 87 | }, 88 | { 89 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Linux_x86_64.tar.gz", 90 | "checksum": "25D0A8A0E64E50D84503332F498D2AB54979145AC97D5457E6B1DDE79212E4FF", 91 | "algorithm": "sha256" 92 | }, 93 | { 94 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Windows_arm64.tar.gz", 95 | "checksum": "0068BD16540746E808544DFA2745A39F9A188F6BF19B50198FCF8B9DCA28371F", 96 | "algorithm": "sha256" 97 | }, 98 | { 99 | "id": "github_release/github.com/reviewdog/reviewdog/v0.20.2/reviewdog_0.20.2_Windows_x86_64.tar.gz", 100 | "checksum": "B5344494EA629A063914149C4DD1AEDB052FCA4EFCAB61B108EF0560C3449632", 101 | "algorithm": "sha256" 102 | }, 103 | { 104 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_darwin_amd64.tar.gz", 105 | "checksum": "63A3BA90EE2325AFAD3FF2E64A4D80688C261E6C68BE8E6AB91214637BF936B8", 106 | "algorithm": "sha256" 107 | }, 108 | { 109 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_darwin_arm64.tar.gz", 110 | "checksum": "CBD193BB490F598D77E179261D7B76DFEBD049DDDEDE5803BA21CBF6A469AEEE", 111 | "algorithm": "sha256" 112 | }, 113 | { 114 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_linux_amd64.tar.gz", 115 | "checksum": "FC0A6886BBB9A23A39EEEC4B176193CADB54DDBE77CDBB19B637933919545395", 116 | "algorithm": "sha256" 117 | }, 118 | { 119 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_linux_arm64.tar.gz", 120 | "checksum": "EDE03682DC955381D057DDE95BB85CE9CA418122209A8A313B617D4ADEC56416", 121 | "algorithm": "sha256" 122 | }, 123 | { 124 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_windows_amd64.zip", 125 | "checksum": "CEF5EA561B9BE20CFF390301C4062C1D7B260AF2F9963C26B406E57AAF30E4D8", 126 | "algorithm": "sha256" 127 | }, 128 | { 129 | "id": "github_release/github.com/rhysd/actionlint/v1.7.4/actionlint_1.7.4_windows_arm64.zip", 130 | "checksum": "205A2F99BE1CAA70CF0558561FAE47F099B5EF85CA57B00ABCE18C840452EAF3", 131 | "algorithm": "sha256" 132 | }, 133 | { 134 | "id": "github_release/github.com/sigstore/cosign/v2.4.1/cosign-darwin-amd64", 135 | "checksum": "666032CA283DA92B6F7953965688FD51200FDC891A86C19E05C98B898EA0AF4E", 136 | "algorithm": "sha256" 137 | }, 138 | { 139 | "id": "github_release/github.com/sigstore/cosign/v2.4.1/cosign-darwin-arm64", 140 | "checksum": "13343856B69F70388C4FE0B986A31DDE5958E444B41BE22D785D3DC5E1A9CC62", 141 | "algorithm": "sha256" 142 | }, 143 | { 144 | "id": "github_release/github.com/sigstore/cosign/v2.4.1/cosign-linux-amd64", 145 | "checksum": "8B24B946DD5809C6BD93DE08033BCF6BC0ED7D336B7785787C080F574B89249B", 146 | "algorithm": "sha256" 147 | }, 148 | { 149 | "id": "github_release/github.com/sigstore/cosign/v2.4.1/cosign-linux-arm64", 150 | "checksum": "3B2E2E3854D0356C45FE6607047526CCD04742D20BD44AFB5BE91FA2A6E7CB4A", 151 | "algorithm": "sha256" 152 | }, 153 | { 154 | "id": "github_release/github.com/sigstore/cosign/v2.4.1/cosign-windows-amd64.exe", 155 | "checksum": "8D57F8A42A981D27290C4227271FA9F0F62CA6630EB4A21D316BD6B01405B87C", 156 | "algorithm": "sha256" 157 | }, 158 | { 159 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_darwin_amd64.tar.gz", 160 | "checksum": "94C2CCFC4330DBF8D587A1127E0CF6BABC618506CBA0D89BF0975620FAF26E60", 161 | "algorithm": "sha256" 162 | }, 163 | { 164 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_darwin_arm64.tar.gz", 165 | "checksum": "EAB029A0ABC4B33228853BF9B6CDEC1A42284326AD6F6B96BF1CAA6CDAFEA407", 166 | "algorithm": "sha256" 167 | }, 168 | { 169 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_linux_amd64.tar.gz", 170 | "checksum": "CF6B642829175BDC64DF16D04B7FAA953B684F4290557ADE4BEF9B661F3F4452", 171 | "algorithm": "sha256" 172 | }, 173 | { 174 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_linux_arm64.tar.gz", 175 | "checksum": "2FD795E2E91BD60A9B8DA4328850B655DE7D7E7BAF8826DDAD4BEAF97291AB25", 176 | "algorithm": "sha256" 177 | }, 178 | { 179 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_windows_amd64.tar.gz", 180 | "checksum": "AF287C68213B1C29CEDE06A0308003D6EE0A015FF89A455858DF5D3E4838BCCB", 181 | "algorithm": "sha256" 182 | }, 183 | { 184 | "id": "github_release/github.com/suzuki-shunsuke/cmdx/v1.7.4/cmdx_windows_arm64.tar.gz", 185 | "checksum": "40F31B6E5926B949148A30957D5DD8761F3579433161EB9AA2627AD1009DB46E", 186 | "algorithm": "sha256" 187 | }, 188 | { 189 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.0.0/ghalint_1.0.0_darwin_amd64.tar.gz", 190 | "checksum": "707BB2D6D4564A950F7AD3A5B3340B0B5BC666FE82B32BD51A5FE0FEF7268804", 191 | "algorithm": "sha256" 192 | }, 193 | { 194 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.0.0/ghalint_1.0.0_darwin_arm64.tar.gz", 195 | "checksum": "3E3FDA71FFAE83CF713295DF2BEF09FC268811DEAB11DEA58D8CAA287642C9DC", 196 | "algorithm": "sha256" 197 | }, 198 | { 199 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.0.0/ghalint_1.0.0_linux_amd64.tar.gz", 200 | "checksum": "E9006FF212A3B27A99AF43DB687DED78173BAA2F9816B2E2A9BED03A2ED2F954", 201 | "algorithm": "sha256" 202 | }, 203 | { 204 | "id": "github_release/github.com/suzuki-shunsuke/ghalint/v1.0.0/ghalint_1.0.0_linux_arm64.tar.gz", 205 | "checksum": "73D110480FB94DB4F5F46980659FCAFB8146ACC0B49D0D3AA6D79EE161C97917", 206 | "algorithm": "sha256" 207 | }, 208 | { 209 | "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.221.0/registry.yaml", 210 | "checksum": "6AB5B060EB966C63B4C8F626684181EF576CAC31105E8FF50AC06C6F7073DA378B9C2C96083A2D9FD7467CE54F026CD0465FEE82F0E6454296891283F6E0FC36", 211 | "algorithm": "sha512" 212 | } 213 | ] 214 | } 215 | -------------------------------------------------------------------------------- /aqua.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # aqua - Declarative CLI Version Manager 3 | # https://aquaproj.github.io/ 4 | checksum: 5 | # Enable Checksum Verification 6 | # https://aquaproj.github.io/docs/tutorial-extras/checksum/ 7 | enabled: true 8 | require_checksum: true 9 | registries: 10 | - type: standard 11 | ref: v4.221.0 # renovate: depName=aquaproj/aqua-registry 12 | packages: 13 | # Split packages per package with `import` to avoid pull requests' conflict 14 | - import: aqua/*.yaml 15 | -------------------------------------------------------------------------------- /aqua/actionlint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: rhysd/actionlint@v1.7.4 3 | -------------------------------------------------------------------------------- /aqua/cmdx.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/cmdx@v1.7.4 3 | -------------------------------------------------------------------------------- /aqua/cosign.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: sigstore/cosign@v2.4.1 3 | -------------------------------------------------------------------------------- /aqua/ghalint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: suzuki-shunsuke/ghalint@v1.0.0 3 | -------------------------------------------------------------------------------- /aqua/ghcp.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: int128/ghcp@v1.13.5 3 | -------------------------------------------------------------------------------- /aqua/golangci-lint.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: golangci/golangci-lint@v1.62.0 3 | -------------------------------------------------------------------------------- /aqua/goreleaser.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: goreleaser/goreleaser@v2.4.8 3 | -------------------------------------------------------------------------------- /aqua/reviewdog.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - name: reviewdog/reviewdog@v0.20.2 3 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | repo_name=${1:-} 8 | if [ -z "$repo_name" ]; then 9 | echo "the repository name is required" >&2 10 | exit 1 11 | fi 12 | 13 | mkdir -p bin 14 | curl -L -o bin/cc-test-reporter https://codeclimate.com/downloads/test-reporter/test-reporter-0.6.3-linux-amd64 15 | chmod a+x bin/cc-test-reporter 16 | export PATH="$PWD/bin:$PATH" 17 | bash scripts/test-code-climate.sh "$repo_name" 18 | -------------------------------------------------------------------------------- /cmd/circleci-config-merge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/sirupsen/logrus" 10 | "github.com/suzuki-shunsuke/circleci-config-merge/pkg/cli" 11 | ) 12 | 13 | var ( 14 | version = "" 15 | commit = "" //nolint:gochecknoglobals 16 | date = "" //nolint:gochecknoglobals 17 | ) 18 | 19 | func main() { 20 | if err := core(); err != nil { 21 | logrus.Fatal(err) 22 | } 23 | } 24 | 25 | func core() error { 26 | runner := cli.Runner{ 27 | LDFlags: &cli.LDFlags{ 28 | Version: version, 29 | Commit: commit, 30 | Date: date, 31 | }, 32 | } 33 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) 34 | defer stop() 35 | return runner.Run(ctx, os.Args...) //nolint:wrapcheck 36 | } 37 | -------------------------------------------------------------------------------- /examples/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Don't edit .circleci/config.yml directly! 2 | # .circleci/config.yml is generated by the command. 3 | # 4 | # $ bash generate.sh 5 | # 6 | # .circleci/config.yml is being split per service. 7 | # If you want to update .circleci/config.yml, update split files and run 8 | # $ bash generate.sh 9 | # and commit split files and .circleci/config.yml. 10 | # 11 | commands: 12 | bar: 13 | steps: 14 | - run: 15 | command: echo bar 16 | name: bar 17 | foo: 18 | steps: 19 | - run: 20 | command: echo foo 21 | name: foo 22 | executors: 23 | bar: 24 | docker: 25 | - image: alpine:3.14.2 26 | foo: 27 | docker: 28 | - image: alpine:3.14.2 29 | jobs: 30 | bar: 31 | executor: bar 32 | steps: 33 | - bar 34 | foo: 35 | executor: foo 36 | steps: 37 | - foo 38 | workflows: 39 | bar: 40 | jobs: 41 | - bar 42 | build: 43 | jobs: 44 | - bar 45 | - foo 46 | foo: 47 | jobs: 48 | - foo 49 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | The following command generates .circleci/config.yml by merging split files. 4 | 5 | ``` 6 | $ bash generate.sh 7 | ``` 8 | -------------------------------------------------------------------------------- /examples/bar/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | bar: 3 | steps: 4 | - run: 5 | name: bar 6 | command: echo bar 7 | executors: 8 | bar: 9 | docker: 10 | - image: alpine:3.14.2 11 | workflows: 12 | build: 13 | jobs: 14 | - bar 15 | bar: 16 | jobs: 17 | - bar 18 | jobs: 19 | bar: 20 | executor: bar 21 | steps: 22 | - bar 23 | -------------------------------------------------------------------------------- /examples/foo/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | commands: 2 | foo: 3 | steps: 4 | - run: 5 | name: foo 6 | command: echo foo 7 | executors: 8 | foo: 9 | docker: 10 | - image: alpine:3.14.2 11 | workflows: 12 | build: 13 | jobs: 14 | - foo 15 | foo: 16 | jobs: 17 | - foo 18 | jobs: 19 | foo: 20 | executor: foo 21 | steps: 22 | - foo 23 | -------------------------------------------------------------------------------- /examples/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | 5 | cd "$(dirname "$0")" 6 | 7 | cat header.txt > .circleci/config.yml 8 | bash merge.sh >> .circleci/config.yml 9 | -------------------------------------------------------------------------------- /examples/header.txt: -------------------------------------------------------------------------------- 1 | # Don't edit .circleci/config.yml directly! 2 | # .circleci/config.yml is generated by the command. 3 | # 4 | # $ bash generate.sh 5 | # 6 | # .circleci/config.yml is being split per service. 7 | # If you want to update .circleci/config.yml, update split files and run 8 | # $ bash generate.sh 9 | # and commit split files and .circleci/config.yml. 10 | # 11 | -------------------------------------------------------------------------------- /examples/merge.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | cd "$(dirname "$0")" 7 | 8 | split_files() { 9 | find . -path "*/.circleci/*.yml" | grep -v "^\./\.circleci/" 10 | } 11 | 12 | split_files | xargs circleci-config-merge merge 13 | -------------------------------------------------------------------------------- /githooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | cmdx test || exit 1 2 | cmdx lint 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/suzuki-shunsuke/circleci-config-merge 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/go-cmp v0.6.0 7 | github.com/mitchellh/mapstructure v1.5.0 8 | github.com/sirupsen/logrus v1.9.3 9 | github.com/stretchr/testify v1.9.0 10 | github.com/urfave/cli/v2 v2.27.5 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 19 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 20 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 9 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 13 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 14 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 15 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 19 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 20 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 21 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 22 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 23 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 24 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 25 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 30 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 31 | -------------------------------------------------------------------------------- /pkg/cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/suzuki-shunsuke/circleci-config-merge/pkg/controller" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func (runner *Runner) setCLIArg(c *cli.Context, params controller.Params) controller.Params { 11 | arr := c.Args().Slice() 12 | args := make(map[string]struct{}, len(arr)) 13 | for _, a := range arr { 14 | args[a] = struct{}{} 15 | } 16 | params.Files = args 17 | return params 18 | } 19 | 20 | func (runner *Runner) action(c *cli.Context) error { 21 | // files... 22 | params := controller.Params{} 23 | params = runner.setCLIArg(c, params) 24 | 25 | ctrl, params, err := controller.New(c.Context, params) 26 | if err != nil { 27 | return fmt.Errorf("initialize a controller: %w", err) 28 | } 29 | 30 | return ctrl.Run(c.Context, ¶ms) //nolint:wrapcheck 31 | } 32 | -------------------------------------------------------------------------------- /pkg/cli/runner.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | type LDFlags struct { 11 | Version string 12 | Commit string 13 | Date string 14 | } 15 | 16 | func (flags *LDFlags) AppVersion() string { 17 | return flags.Version + " (" + flags.Commit + ")" 18 | } 19 | 20 | type Runner struct { 21 | Stdin io.Reader 22 | Stdout io.Writer 23 | Stderr io.Writer 24 | LDFlags *LDFlags 25 | } 26 | 27 | func (runner *Runner) Run(ctx context.Context, args ...string) error { 28 | app := cli.App{ 29 | Name: "circleci-config-merge", 30 | Usage: "generate CircleCI configuration file by merging multiple files. https://github.com/suzuki-shunsuke/circleci-config-merge", 31 | Version: runner.LDFlags.AppVersion(), 32 | Commands: []*cli.Command{ 33 | { 34 | Name: "merge", 35 | Usage: "generate CircleCI configuration file by merging multiple files", 36 | Action: runner.action, 37 | Flags: []cli.Flag{}, 38 | }, 39 | }, 40 | } 41 | 42 | return app.RunContext(ctx, args) //nolint:wrapcheck 43 | } 44 | -------------------------------------------------------------------------------- /pkg/controller/config.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | type Config struct { 11 | Version interface{} `yaml:",omitempty"` 12 | Setup *bool `yaml:",omitempty"` 13 | Orbs map[string]interface{} `yaml:",omitempty"` 14 | Workflows *Workflows 15 | Jobs map[string]interface{} `yaml:",omitempty"` 16 | Commands map[string]interface{} `yaml:",omitempty"` 17 | Executors map[string]interface{} `yaml:",omitempty"` 18 | Parameters map[string]interface{} `yaml:",omitempty"` 19 | } 20 | 21 | func readFile(filePath string, cfg *Config) error { 22 | file, err := os.Open(filePath) 23 | if err != nil { 24 | return fmt.Errorf("open a file %s: %w", filePath, err) 25 | } 26 | defer file.Close() 27 | if err := yaml.NewDecoder(file).Decode(cfg); err != nil { 28 | return fmt.Errorf("parse a file as YAML %s: %w", filePath, err) 29 | } 30 | return nil 31 | } 32 | 33 | func mergeConfig(base, child *Config) *Config { 34 | if child.Version != nil { 35 | base.Version = child.Version 36 | } 37 | if child.Setup != nil { 38 | base.Setup = child.Setup 39 | } 40 | base.Orbs = mergeMap(base.Orbs, child.Orbs) 41 | base.Workflows = mergeWorkflows(base.Workflows, child.Workflows) 42 | base.Jobs = mergeMap(base.Jobs, child.Jobs) 43 | base.Commands = mergeMap(base.Commands, child.Commands) 44 | base.Executors = mergeMap(base.Executors, child.Executors) 45 | base.Parameters = mergeMap(base.Parameters, child.Parameters) 46 | return base 47 | } 48 | -------------------------------------------------------------------------------- /pkg/controller/config_internal_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_mergeConfig(t *testing.T) { 10 | t.Parallel() 11 | data := []struct { 12 | title string 13 | exp *Config 14 | base *Config 15 | child *Config 16 | }{ 17 | { 18 | title: "normal", 19 | exp: &Config{ 20 | Version: "2.0", 21 | Workflows: &Workflows{ 22 | Workflows: map[string]*Workflow{ 23 | "build": { 24 | Jobs: []interface{}{ 25 | "foo", 26 | "bar", 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | base: &Config{ 33 | Version: "2.0", 34 | Workflows: &Workflows{ 35 | Workflows: map[string]*Workflow{ 36 | "build": { 37 | Jobs: []interface{}{ 38 | "foo", 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | child: &Config{ 45 | Version: "2.0", 46 | Workflows: &Workflows{ 47 | Workflows: map[string]*Workflow{ 48 | "build": { 49 | Jobs: []interface{}{ 50 | "bar", 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | for _, d := range data { 59 | t.Run(d.title, func(t *testing.T) { 60 | t.Parallel() 61 | m := mergeConfig(d.base, d.child) 62 | require.Equal(t, d.exp, m) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/controller/config_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/stretchr/testify/require" 9 | "github.com/suzuki-shunsuke/circleci-config-merge/pkg/controller" 10 | "gopkg.in/yaml.v3" 11 | ) 12 | 13 | func testMarshalYAML(exp, data interface{}) (string, error) { 14 | b, err := yaml.Marshal(data) 15 | if err != nil { 16 | return "", fmt.Errorf("marshal data as YAML: %w", err) 17 | } 18 | var cfgMap interface{} 19 | if err := yaml.Unmarshal(b, &cfgMap); err != nil { 20 | return "", fmt.Errorf("unmarshal data as YAML: %w", err) 21 | } 22 | var expMap interface{} 23 | if s, ok := exp.(string); ok { 24 | if err := yaml.Unmarshal([]byte(s), &expMap); err != nil { 25 | return "", fmt.Errorf("unmarshal exp as YAML: %w", err) 26 | } 27 | } else { 28 | b, err := yaml.Marshal(exp) 29 | if err != nil { 30 | return "", fmt.Errorf("marshal exp as YAML: %w", err) 31 | } 32 | if err := yaml.Unmarshal(b, &expMap); err != nil { 33 | return "", fmt.Errorf("unmarshal exp as YAML: %w", err) 34 | } 35 | } 36 | return cmp.Diff(expMap, cfgMap), nil 37 | } 38 | 39 | //nolint:funlen 40 | func TestConfig_MasharlYAML(t *testing.T) { 41 | t.Parallel() 42 | data := []struct { 43 | title string 44 | cfg *controller.Config 45 | exp string 46 | }{ 47 | { 48 | title: "normal", 49 | cfg: &controller.Config{ 50 | Version: "2.1", 51 | Orbs: map[string]interface{}{ 52 | "foo": "circleci/hello-build@0.0.5", 53 | }, 54 | Workflows: &controller.Workflows{ 55 | Version: "2", 56 | Workflows: map[string]*controller.Workflow{ 57 | "build": { 58 | When: "<< pipeline.parameters.run_integration_tests >>", 59 | Jobs: []interface{}{ 60 | "foo", "bar", 61 | }, 62 | }, 63 | }, 64 | }, 65 | Jobs: map[string]interface{}{ 66 | "foo": map[string]interface{}{}, 67 | "bar": map[string]interface{}{}, 68 | }, 69 | Commands: map[string]interface{}{ 70 | "test": map[string]interface{}{}, 71 | }, 72 | Executors: map[string]interface{}{ 73 | "golang": map[string]interface{}{}, 74 | }, 75 | }, 76 | exp: ` 77 | version: "2.1" 78 | orbs: 79 | foo: circleci/hello-build@0.0.5 80 | workflows: 81 | version: "2" 82 | build: 83 | when: << pipeline.parameters.run_integration_tests >> 84 | jobs: 85 | - foo 86 | - bar 87 | jobs: 88 | foo: {} 89 | bar: {} 90 | commands: 91 | test: {} 92 | executors: 93 | golang: {} 94 | `, 95 | }, 96 | { 97 | title: "logical statement", 98 | // https://circleci.com/docs/configuration-reference/#logic-statement-examples 99 | cfg: &controller.Config{ 100 | Version: "2.1", 101 | Orbs: map[string]interface{}{ 102 | "foo": "circleci/hello-build@0.0.5", 103 | }, 104 | Workflows: &controller.Workflows{ 105 | Version: "2", 106 | Workflows: map[string]*controller.Workflow{ 107 | "build": { 108 | When: map[interface{}]interface{}{ 109 | "or": []map[interface{}]interface{}{ 110 | { 111 | "equal": []interface{}{ 112 | "main", 113 | "<< pipeline.git.branch >>", 114 | }, 115 | }, 116 | { 117 | "equal": []interface{}{ 118 | "staging", 119 | "<< pipeline.git.branch >>", 120 | }, 121 | }, 122 | }, 123 | }, 124 | Jobs: []interface{}{ 125 | "foo", "bar", 126 | }, 127 | }, 128 | }, 129 | }, 130 | Jobs: map[string]interface{}{ 131 | "foo": map[string]interface{}{}, 132 | "bar": map[string]interface{}{}, 133 | }, 134 | Commands: map[string]interface{}{ 135 | "test": map[string]interface{}{}, 136 | }, 137 | Executors: map[string]interface{}{ 138 | "golang": map[string]interface{}{}, 139 | }, 140 | }, 141 | exp: ` 142 | version: "2.1" 143 | orbs: 144 | foo: circleci/hello-build@0.0.5 145 | workflows: 146 | version: "2" 147 | build: 148 | when: 149 | or: 150 | - equal: [ main, << pipeline.git.branch >> ] 151 | - equal: [ staging, << pipeline.git.branch >> ] 152 | jobs: 153 | - foo 154 | - bar 155 | jobs: 156 | foo: {} 157 | bar: {} 158 | commands: 159 | test: {} 160 | executors: 161 | golang: {} 162 | `, 163 | }, 164 | } 165 | for _, d := range data { 166 | t.Run(d.title, func(t *testing.T) { 167 | t.Parallel() 168 | diff, err := testMarshalYAML(d.exp, d.cfg) 169 | require.NoError(t, err) 170 | if diff != "" { 171 | t.Fatal(diff) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | type Params struct { 12 | Files map[string]struct{} 13 | } 14 | 15 | func New(_ context.Context, params Params) (Controller, Params, error) { 16 | return Controller{ 17 | Stdout: os.Stdout, 18 | Stderr: os.Stderr, 19 | }, params, nil 20 | } 21 | 22 | func (ctrl *Controller) Run(_ context.Context, params *Params) error { 23 | var cfg *Config 24 | for filePath := range params.Files { 25 | child := &Config{} 26 | if err := readFile(filePath, child); err != nil { 27 | return fmt.Errorf("read a file "+filePath+": %w", err) 28 | } 29 | if cfg == nil { 30 | cfg = child 31 | continue 32 | } 33 | cfg = mergeConfig(cfg, child) 34 | } 35 | for k, workflow := range cfg.Workflows.Workflows { 36 | jobs, err := sortJobs(workflow.Jobs) 37 | if err == nil { 38 | workflow.Jobs = jobs 39 | } 40 | cfg.Workflows.Workflows[k] = workflow 41 | } 42 | if err := yaml.NewEncoder(ctrl.Stdout).Encode(&cfg); err != nil { 43 | return fmt.Errorf("encode a merged config as YAML: %w", err) 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/controller/job.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | ) 8 | 9 | func getJobName(job interface{}) (string, error) { 10 | switch v := job.(type) { 11 | case string: 12 | return v, nil 13 | case map[interface{}]interface{}: 14 | for jobName, jobValue := range v { 15 | a, ok := jobValue.(map[interface{}]interface{}) 16 | if !ok { 17 | return "", fmt.Errorf("workflow job's element must be map: %+v", jobValue) 18 | } 19 | if name, ok := a["name"]; ok { 20 | jobName = name 21 | } 22 | s, ok := jobName.(string) 23 | if !ok { 24 | return "", fmt.Errorf("workflow job's name must be string: %+v", jobName) 25 | } 26 | return s, nil 27 | } 28 | return "", errors.New("workflow job's element is empty") 29 | default: 30 | return "", errors.New("workflow job must be string or map") 31 | } 32 | } 33 | 34 | func sortJobs(jobs []interface{}) ([]interface{}, error) { 35 | type WorkflowJob struct { 36 | Name string 37 | Job interface{} 38 | } 39 | wfJobs := make([]WorkflowJob, len(jobs)) 40 | for i, job := range jobs { 41 | name, err := getJobName(job) 42 | if err != nil { 43 | return nil, fmt.Errorf("get a job name: %w", err) 44 | } 45 | wfJobs[i] = WorkflowJob{ 46 | Name: name, 47 | Job: job, 48 | } 49 | } 50 | 51 | sort.Slice(wfJobs, func(i, j int) bool { 52 | return wfJobs[i].Name < wfJobs[j].Name 53 | }) 54 | arr := make([]interface{}, len(jobs)) 55 | for i, job := range wfJobs { 56 | arr[i] = job.Job 57 | } 58 | return arr, nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/controller/job_internal_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_getJobName(t *testing.T) { 10 | t.Parallel() 11 | data := []struct { 12 | title string 13 | exp string 14 | isErr bool 15 | job interface{} 16 | }{ 17 | { 18 | title: "normal", 19 | exp: "foo", 20 | job: "foo", 21 | }, 22 | { 23 | title: "map", 24 | exp: "zoo", 25 | job: map[interface{}]interface{}{ 26 | "zoo": map[interface{}]interface{}{ 27 | "requires": []interface{}{ 28 | "bar", 29 | }, 30 | }, 31 | }, 32 | }, 33 | } 34 | for _, d := range data { 35 | t.Run(d.title, func(t *testing.T) { 36 | t.Parallel() 37 | name, err := getJobName(d.job) 38 | if d.isErr { 39 | require.Error(t, err) 40 | return 41 | } 42 | require.NoError(t, err) 43 | require.Equal(t, d.exp, name) 44 | }) 45 | } 46 | } 47 | 48 | func Test_sortJobs(t *testing.T) { 49 | t.Parallel() 50 | data := []struct { 51 | title string 52 | exp []interface{} 53 | isErr bool 54 | jobs []interface{} 55 | }{ 56 | { 57 | title: "normal", 58 | exp: []interface{}{ 59 | "bar", 60 | "foo", 61 | }, 62 | jobs: []interface{}{ 63 | "foo", 64 | "bar", 65 | }, 66 | }, 67 | { 68 | title: "map", 69 | exp: []interface{}{ 70 | "foo", 71 | map[interface{}]interface{}{ 72 | "zoo": map[interface{}]interface{}{ 73 | "requires": []interface{}{ 74 | "bar", 75 | }, 76 | }, 77 | }, 78 | }, 79 | jobs: []interface{}{ 80 | map[interface{}]interface{}{ 81 | "zoo": map[interface{}]interface{}{ 82 | "requires": []interface{}{ 83 | "bar", 84 | }, 85 | }, 86 | }, 87 | "foo", 88 | }, 89 | }, 90 | } 91 | for _, d := range data { 92 | t.Run(d.title, func(t *testing.T) { 93 | t.Parallel() 94 | jobs, err := sortJobs(d.jobs) 95 | if d.isErr { 96 | require.Error(t, err) 97 | return 98 | } 99 | require.NoError(t, err) 100 | require.Equal(t, d.exp, jobs) 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /pkg/controller/map.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | func mergeMap(base, child map[string]interface{}) map[string]interface{} { 4 | if base == nil { 5 | return child 6 | } 7 | for k, v := range child { 8 | base[k] = v 9 | } 10 | return base 11 | } 12 | -------------------------------------------------------------------------------- /pkg/controller/map_internal_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_mergeMap(t *testing.T) { 10 | t.Parallel() 11 | data := []struct { 12 | title string 13 | exp map[string]interface{} 14 | base map[string]interface{} 15 | child map[string]interface{} 16 | }{ 17 | { 18 | title: "normal", 19 | exp: map[string]interface{}{ 20 | "foo": struct{}{}, 21 | "bar": struct{}{}, 22 | }, 23 | base: map[string]interface{}{ 24 | "foo": struct{}{}, 25 | }, 26 | child: map[string]interface{}{ 27 | "bar": struct{}{}, 28 | }, 29 | }, 30 | } 31 | for _, d := range data { 32 | t.Run(d.title, func(t *testing.T) { 33 | t.Parallel() 34 | m := mergeMap(d.base, d.child) 35 | require.Equal(t, d.exp, m) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/controller/new.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Controller struct { 8 | Stdout io.Writer 9 | Stderr io.Writer 10 | } 11 | -------------------------------------------------------------------------------- /pkg/controller/workflow.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | type Workflow struct { 4 | Triggers interface{} `yaml:",omitempty"` 5 | Jobs []interface{} `yaml:",omitempty"` 6 | When interface{} `yaml:",omitempty"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/controller/workflow_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/suzuki-shunsuke/circleci-config-merge/pkg/controller" 8 | ) 9 | 10 | func TestWorkflow_MasharlYAML(t *testing.T) { 11 | t.Parallel() 12 | data := []struct { 13 | title string 14 | wf *controller.Workflow 15 | exp string 16 | }{ 17 | { 18 | title: "normal", 19 | wf: &controller.Workflow{ 20 | Jobs: []interface{}{ 21 | "foo", 22 | }, 23 | }, 24 | exp: ` 25 | jobs: 26 | - foo 27 | `, 28 | }, 29 | } 30 | for _, d := range data { 31 | t.Run(d.title, func(t *testing.T) { 32 | t.Parallel() 33 | diff, err := testMarshalYAML(d.exp, d.wf) 34 | require.NoError(t, err) 35 | if diff != "" { 36 | t.Fatal(diff) 37 | } 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/controller/workflows.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mitchellh/mapstructure" 7 | ) 8 | 9 | type Workflows struct { 10 | Version interface{} `yaml:",omitempty"` 11 | Workflows map[string]*Workflow `yaml:",omitempty"` 12 | } 13 | 14 | func mergeWorkflows(base, child *Workflows) *Workflows { 15 | if base == nil { 16 | return child 17 | } 18 | if child == nil { 19 | return base 20 | } 21 | if child.Version != nil { 22 | base.Version = child.Version 23 | } 24 | for k, childWorkflow := range child.Workflows { 25 | if baseWorkflow, ok := base.Workflows[k]; ok { 26 | baseWorkflow.Jobs = append(baseWorkflow.Jobs, childWorkflow.Jobs...) 27 | if childWorkflow.Triggers != nil { 28 | baseWorkflow.Triggers = childWorkflow.Triggers 29 | } 30 | base.Workflows[k] = baseWorkflow 31 | } else { 32 | if base.Workflows == nil { 33 | base.Workflows = map[string]*Workflow{ 34 | k: childWorkflow, 35 | } 36 | } else { 37 | base.Workflows[k] = childWorkflow 38 | } 39 | } 40 | } 41 | return base 42 | } 43 | 44 | func (wfs *Workflows) MarshalYAML() (interface{}, error) { 45 | m := make(map[string]interface{}, len(wfs.Workflows)) 46 | for k, v := range wfs.Workflows { 47 | m[k] = v 48 | } 49 | if wfs.Version != nil { 50 | m["version"] = wfs.Version 51 | } 52 | return m, nil 53 | } 54 | 55 | func (wfs *Workflows) UnmarshalYAML(unmarshal func(interface{}) error) error { 56 | m := map[string]interface{}{} 57 | if err := unmarshal(&m); err != nil { 58 | return err 59 | } 60 | if version, ok := m["version"]; ok { 61 | wfs.Version = version 62 | delete(m, "version") 63 | } 64 | wfMap := map[string]*Workflow{} 65 | if err := mapstructure.Decode(m, &wfMap); err != nil { 66 | return fmt.Errorf("decode map to structure: %w", err) 67 | } 68 | wfs.Workflows = wfMap 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/controller/workflows_internal_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_mergeWorkflows(t *testing.T) { 10 | t.Parallel() 11 | data := []struct { 12 | title string 13 | exp *Workflows 14 | base *Workflows 15 | child *Workflows 16 | }{ 17 | { 18 | title: "normal", 19 | exp: &Workflows{ 20 | Version: "2.0", 21 | Workflows: map[string]*Workflow{ 22 | "build": { 23 | Jobs: []interface{}{ 24 | "foo", 25 | "bar", 26 | }, 27 | }, 28 | }, 29 | }, 30 | base: &Workflows{ 31 | Version: "2.0", 32 | Workflows: map[string]*Workflow{ 33 | "build": { 34 | Jobs: []interface{}{ 35 | "foo", 36 | }, 37 | }, 38 | }, 39 | }, 40 | child: &Workflows{ 41 | Workflows: map[string]*Workflow{ 42 | "build": { 43 | Jobs: []interface{}{ 44 | "bar", 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | } 51 | for _, d := range data { 52 | t.Run(d.title, func(t *testing.T) { 53 | t.Parallel() 54 | m := mergeWorkflows(d.base, d.child) 55 | require.Equal(t, d.exp, m) 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/controller/workflows_test.go: -------------------------------------------------------------------------------- 1 | package controller_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | "github.com/suzuki-shunsuke/circleci-config-merge/pkg/controller" 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | func TestWorkflows_UnmasharlYAML(t *testing.T) { 12 | t.Parallel() 13 | data := []struct { 14 | title string 15 | exp controller.Workflows 16 | yaml []byte 17 | isErr bool 18 | }{ 19 | { 20 | title: "normal", 21 | exp: controller.Workflows{ 22 | Version: "2.1", 23 | Workflows: map[string]*controller.Workflow{ 24 | "build": { 25 | Jobs: []interface{}{ 26 | "foo", 27 | }, 28 | }, 29 | }, 30 | }, 31 | yaml: []byte(` 32 | version: "2.1" 33 | build: 34 | jobs: 35 | - foo 36 | `), 37 | }, 38 | } 39 | for _, d := range data { 40 | t.Run(d.title, func(t *testing.T) { 41 | t.Parallel() 42 | wfs := controller.Workflows{} 43 | err := yaml.Unmarshal(d.yaml, &wfs) 44 | if d.isErr { 45 | require.Error(t, err) 46 | return 47 | } 48 | require.NoError(t, err) 49 | require.Equal(t, d.exp, wfs) 50 | }) 51 | } 52 | } 53 | 54 | func TestWorkflows_MasharlYAML(t *testing.T) { 55 | t.Parallel() 56 | data := []struct { 57 | title string 58 | wfs *controller.Workflows 59 | exp interface{} 60 | }{ 61 | { 62 | title: "normal", 63 | wfs: &controller.Workflows{ 64 | Version: "2.1", 65 | Workflows: map[string]*controller.Workflow{ 66 | "build": { 67 | When: "<< pipeline.parameters.run_integration_tests >>", 68 | Jobs: []interface{}{ 69 | "foo", 70 | }, 71 | }, 72 | }, 73 | }, 74 | exp: ` 75 | version: "2.1" 76 | build: 77 | when: << pipeline.parameters.run_integration_tests >> 78 | jobs: 79 | - foo 80 | `, 81 | }, 82 | } 83 | for _, d := range data { 84 | t.Run(d.title, func(t *testing.T) { 85 | t.Parallel() 86 | diff, err := testMarshalYAML(d.exp, d.wfs) 87 | require.NoError(t, err) 88 | if diff != "" { 89 | t.Fatal(diff) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: [ 3 | "config:best-practices", 4 | "github>suzuki-shunsuke/renovate-config#2.5.0", 5 | "github>suzuki-shunsuke/renovate-config:nolimit#2.5.0", 6 | "github>suzuki-shunsuke/renovate-config:action-go-version#2.5.0", 7 | "github>aquaproj/aqua-renovate-config#2.4.0", 8 | "github>aquaproj/aqua-renovate-config:file#2.4.0(aqua/.*\\.ya?ml)", 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | mkdir -p ".coverage/$1" 2 | go test "./$1" -coverprofile=".coverage/$1/coverage.txt" -covermode=atomic 3 | go tool cover -html=".coverage/$1/coverage.txt" 4 | -------------------------------------------------------------------------------- /scripts/fmt.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | cd "$(dirname "$0")/.." 7 | 8 | git ls-files | grep -E ".*\.go$" | xargs gofmt -l -s -w 9 | -------------------------------------------------------------------------------- /scripts/githook.sh: -------------------------------------------------------------------------------- 1 | echoEval() { 2 | echo "+ $@" 3 | eval "$@" 4 | } 5 | 6 | cd `dirname $0`/.. 7 | if [ ! -f .git/hooks/pre-commit ]; then 8 | echoEval ln -s ../../githooks/pre-commit.sh .git/hooks/pre-commit || exit 1 9 | fi 10 | echoEval chmod a+x githooks/* 11 | -------------------------------------------------------------------------------- /scripts/test-code-climate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | set -o pipefail 5 | 6 | ee() { 7 | echo "+ $*" 8 | eval "$@" 9 | } 10 | 11 | cd "$(dirname "$0")/.." 12 | 13 | repo_name=${1:-} 14 | if [ -z "$repo_name" ]; then 15 | echo "the repository name is required" >&2 16 | exit 1 17 | fi 18 | 19 | ee cc-test-reporter before-build 20 | 21 | mkdir -p .code-climate 22 | 23 | for d in $(go list ./...); do 24 | echo "$d" 25 | profile=.code-climate/$d/profile.txt 26 | coverage=.code-climate/$d/coverage.json 27 | ee mkdir -p "$(dirname "$profile")" "$(dirname "$coverage")" 28 | ee go test -race -coverprofile="$profile" -covermode=atomic "$d" 29 | if [ "$(wc -l < "$profile")" -eq 1 ]; then 30 | continue 31 | fi 32 | ee cc-test-reporter format-coverage -t gocov -p "github.com/suzuki-shunsuke/${repo_name}" -o "$coverage" "$profile" 33 | done 34 | 35 | result=.code-climate/codeclimate.total.json 36 | # shellcheck disable=SC2046 37 | ee cc-test-reporter sum-coverage $(find .code-climate -name coverage.json) -o "$result" 38 | ee cc-test-reporter upload-coverage -i "$result" 39 | --------------------------------------------------------------------------------