├── .gitignore ├── .github ├── pull_request_template.md ├── workflows │ ├── dependency-review.yaml │ ├── test.yaml │ ├── dependabot-automerge.yaml │ ├── coverage.yaml │ ├── lint.yaml │ ├── build.yaml │ └── release.yaml ├── dependency-review-config.yaml ├── dependabot.yaml └── commitlint.config.mjs ├── Dockerfile ├── .goreleaser.yaml ├── entrypoint.sh ├── Makefile ├── action.yaml ├── cmd └── ccv │ └── main.go ├── go.mod ├── README.md ├── ccv.go ├── ccv_test.go ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /cover.out 3 | /cover.out.raw 4 | /sbom.spdx.json 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 AS builder 2 | COPY . . 3 | RUN apk --no-cache add go && CGO_ENABLED=0 go build ./cmd/ccv 4 | FROM alpine:3.22@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412 5 | RUN apk --no-cache add git 6 | COPY --from=builder ccv /usr/local/bin/ 7 | COPY entrypoint.sh /entrypoint.sh 8 | ENTRYPOINT ["/entrypoint.sh"] 9 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - id: ccv 4 | binary: ccv 5 | main: ./cmd/ccv 6 | ldflags: 7 | - > 8 | -s -w 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | goarch: 15 | - amd64 16 | - arm64 17 | 18 | changelog: 19 | use: github-native 20 | 21 | release: 22 | extra_files: 23 | - glob: "{{ .Env.GITHUB_SBOM_PATH }}" 24 | name_template: "{{ .ProjectName }}.v{{ .Version }}.sbom.spdx.json" 25 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yaml: -------------------------------------------------------------------------------- 1 | name: dependency review 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | dependency-review: 9 | permissions: 10 | contents: read 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 14 | - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 15 | with: 16 | config-file: .github/dependency-review-config.yaml 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | test-go: 9 | permissions: 10 | contents: read 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 14 | with: 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 17 | with: 18 | go-version: stable 19 | - run: | 20 | git config --global user.email "test@example.com" 21 | git config --global user.name "Test" 22 | - run: go test -v ./... 23 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | # if the first argument to the script is "true", it will push a tag to the repository. 4 | WRITE_TAG="$1" 5 | # the runner workspace will be mounted here, and git complains otherwise 6 | git config --global --add safe.directory /github/workspace 7 | # if the ccv tag exists, just exit 8 | if [ "$(git tag -l "$(ccv)")" ]; then 9 | echo "new-tag=false" >>"$GITHUB_OUTPUT" 10 | exit 11 | fi 12 | # if it doesn't, tag and push 13 | if [ "$WRITE_TAG" = "true" ]; then 14 | git tag "$(ccv)" 15 | git push --tags 16 | fi 17 | { 18 | echo "new-tag=true" 19 | echo "new-tag-version=$(ccv)" 20 | echo "new-tag-version-type=$(ccv --version-type)" 21 | } >>"$GITHUB_OUTPUT" 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: mod-tidy 3 | export GIT_CONFIG_NOSYSTEM=true GIT_CONFIG_GLOBAL=/tmp/gitconfig \ 4 | && git config --global user.email "test@example.com" \ 5 | && git config --global user.name "Test" \ 6 | && go test -v ./... -count=1 7 | 8 | .PHONY: mod-tidy 9 | mod-tidy: 10 | go mod tidy 11 | 12 | .PHONY: build 13 | build: 14 | goreleaser build --clean --debug --single-target --snapshot 15 | 16 | .PHONY: lint 17 | lint: 18 | golangci-lint run --enable gocritic 19 | 20 | .PHONY: cover 21 | cover: mod-tidy generate 22 | go test -v -covermode=atomic -coverprofile=cover.out.raw -coverpkg=./... ./... 23 | grep -Ev 'internal/mock|_enumer.go' cover.out.raw > cover.out 24 | go tool cover -html=cover.out 25 | -------------------------------------------------------------------------------- /.github/dependency-review-config.yaml: -------------------------------------------------------------------------------- 1 | # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md 2 | allow-licenses: 3 | - 'Apache-2.0' 4 | - 'BSD-2-Clause' 5 | - 'BSD-2-Clause-FreeBSD' 6 | - 'BSD-3-Clause' 7 | - 'ISC' 8 | - 'MIT' 9 | - 'PostgreSQL' 10 | - 'Python-2.0' 11 | - 'X11' 12 | - 'Zlib' 13 | 14 | allow-dependencies-licenses: 15 | # this action is GPL-3 but it is only used in CI 16 | # https://github.com/actions/dependency-review-action/issues/530#issuecomment-1638291806 17 | - pkg:githubactions/vladopajic/go-test-coverage@bcd064e5ceef1ccec5441519eb054263b6a44787 18 | # this package is MPL-2.0 and has a CNCF exception 19 | # https://github.com/cncf/foundation/blob/9b8c9173c2101c1b4aedad3caf2c0128715133f6/license-exceptions/cncf-exceptions-2022-04-12.json#L43C17-L43C47 20 | - pkg:golang/github.com/go-sql-driver/mysql 21 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | commit-message: 5 | prefix: chore 6 | include: scope 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | groups: 11 | github-actions: 12 | patterns: 13 | - "*" 14 | update-types: 15 | - "minor" 16 | - "patch" 17 | - package-ecosystem: docker 18 | commit-message: 19 | prefix: chore 20 | include: scope 21 | directory: / 22 | schedule: 23 | interval: monthly 24 | groups: 25 | docker: 26 | patterns: 27 | - "*" 28 | update-types: 29 | - "minor" 30 | - "patch" 31 | - package-ecosystem: gomod 32 | commit-message: 33 | prefix: chore 34 | include: scope 35 | directory: / 36 | schedule: 37 | interval: monthly 38 | groups: 39 | gomod: 40 | patterns: 41 | - "*" 42 | update-types: 43 | - "minor" 44 | - "patch" 45 | -------------------------------------------------------------------------------- /action.yaml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits Versioner Action 2 | description: Automatically tag a new version based on the commit messages of commits since the last tag. 3 | inputs: 4 | write-tag: 5 | description: If true, and ccv determines that a new version is required, the action will automatically write the new version tag to the repository. 6 | required: false 7 | default: 'true' 8 | outputs: 9 | new-tag: 10 | description: Either "true" or "false" depending on whether a new tag was pushed. 11 | new-tag-version: 12 | description: The new version that was tagged. This will only be set if new-tag=true. 13 | new-tag-version-type: 14 | description: Describes the semantic version type of the new tag. One of "major", "minor", or "patch". This will only be set if new-tag=true. 15 | runs: 16 | using: docker 17 | image: Dockerfile 18 | args: 19 | - ${{ inputs.write-tag }} 20 | branding: 21 | icon: tag 22 | color: blue 23 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request 2 | name: dependabot auto-merge 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | permissions: {} 8 | jobs: 9 | dependabot-automerge: 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | runs-on: ubuntu-latest 14 | if: github.actor == 'dependabot[bot]' 15 | steps: 16 | - name: Fetch dependabot metadata 17 | id: metadata 18 | uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0 19 | with: 20 | github-token: "${{ secrets.GITHUB_TOKEN }}" 21 | - name: Enable auto-merge for Dependabot PRs # these still need approval before merge 22 | run: gh pr merge --auto --merge "$PR_URL" 23 | env: 24 | PR_URL: ${{github.event.pull_request.html_url}} 25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 26 | -------------------------------------------------------------------------------- /.github/commitlint.config.mjs: -------------------------------------------------------------------------------- 1 | /* Taken from: https://github.com/wagoid/commitlint-github-action/blob/7f0a61df502599e1f1f50880aaa7ec1e2c0592f2/commitlint.config.mjs */ 2 | /* eslint-disable import/no-extraneous-dependencies */ 3 | import { maxLineLength } from '@commitlint/ensure' 4 | 5 | const bodyMaxLineLength = 100 6 | 7 | const validateBodyMaxLengthIgnoringDeps = (parsedCommit) => { 8 | const { type, scope, body } = parsedCommit 9 | const isDepsCommit = 10 | type === 'chore' && (scope === 'deps' || scope === 'deps-dev') 11 | 12 | return [ 13 | isDepsCommit || !body || maxLineLength(body, bodyMaxLineLength), 14 | `body's lines must not be longer than ${bodyMaxLineLength}`, 15 | ] 16 | } 17 | 18 | export default { 19 | extends: ['@commitlint/config-conventional'], 20 | plugins: ['commitlint-plugin-function-rules'], 21 | rules: { 22 | 'body-max-line-length': [0], 23 | 'function-rules/body-max-line-length': [ 24 | 2, 25 | 'always', 26 | validateBodyMaxLengthIgnoringDeps, 27 | ], 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /cmd/ccv/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements the ccv command line tool. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/alecthomas/kong" 8 | "github.com/smlx/ccv" 9 | ) 10 | 11 | // CLI represents the command-line interface. 12 | type CLI struct { 13 | VersionType bool `kong:"env='VERSION_TYPE',help='Print new version type (major, minor, patch) instead of version number'"` 14 | } 15 | 16 | // Run implements the CLI logic. 17 | func (cli *CLI) Run() error { 18 | if cli.VersionType { 19 | nextVersionType, err := ccv.NextVersionType(`.`) 20 | if err != nil { 21 | return fmt.Errorf("couldn't get next version type: %v", err) 22 | } 23 | fmt.Println(nextVersionType) 24 | } else { 25 | nextVersion, err := ccv.NextVersion(`.`) 26 | if err != nil { 27 | return fmt.Errorf("couldn't get next version: %v", err) 28 | } 29 | fmt.Println(nextVersion) 30 | } 31 | return nil 32 | } 33 | 34 | func main() { 35 | cli := CLI{} 36 | kctx := kong.Parse(&cli, kong.UsageOnError()) 37 | kctx.FatalIfErrorf(kctx.Run()) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | coverage: 9 | permissions: 10 | contents: write 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 14 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 15 | with: 16 | go-version: stable 17 | - run: | 18 | git config --global user.email "test@example.com" 19 | git config --global user.name "Test" 20 | - name: Calculate coverage 21 | run: | 22 | go test -v -covermode=atomic -coverprofile=cover.out.raw -coverpkg=./... ./... 23 | # remove generated code from coverage calculation 24 | grep -Ev 'internal/mock|_enumer.go' cover.out.raw > cover.out 25 | - name: Generage coverage badge 26 | uses: vladopajic/go-test-coverage@d9ec07b8799c458a7bc1f9b36d14261746d63529 # v2.18.0 27 | with: 28 | profile: cover.out 29 | local-prefix: github.com/${{ github.repository }} 30 | git-token: ${{ secrets.GITHUB_TOKEN }} 31 | # orphan branch for storing badges 32 | git-branch: badges 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smlx/ccv 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/Masterminds/semver/v3 v3.4.0 9 | github.com/alecthomas/kong v1.13.0 10 | github.com/go-git/go-git/v5 v5.16.4 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Microsoft/go-winio v0.6.2 // indirect 16 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 17 | github.com/cloudflare/circl v1.6.1 // indirect 18 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 19 | github.com/emirpasic/gods v1.18.1 // indirect 20 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 21 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 22 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 23 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 24 | github.com/kevinburke/ssh_config v1.2.0 // indirect 25 | github.com/pjbgf/sha1cd v0.3.2 // indirect 26 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 27 | github.com/skeema/knownhosts v1.3.1 // indirect 28 | github.com/xanzy/ssh-agent v0.3.3 // indirect 29 | golang.org/x/crypto v0.45.0 // indirect 30 | golang.org/x/net v0.47.0 // indirect 31 | golang.org/x/sys v0.38.0 // indirect 32 | gopkg.in/warnings.v0 v0.1.2 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | lint-go: 9 | permissions: 10 | contents: read 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 14 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 15 | with: 16 | go-version: stable 17 | - uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 18 | with: 19 | args: --timeout=180s --enable gocritic 20 | lint-commits: 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 27 | with: 28 | fetch-depth: 0 29 | - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6.2.1 30 | with: 31 | configFile: .github/commitlint.config.mjs 32 | lint-actions: 33 | permissions: 34 | contents: read 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 38 | - uses: docker://rhysd/actionlint:1.7.0@sha256:601d6faeefa07683a4a79f756f430a1850b34d575d734b1d1324692202bf312e # v1.7.0 39 | with: 40 | args: -color 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | build-snapshot: 9 | permissions: 10 | contents: read 11 | packages: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 15 | with: 16 | ref: ${{ github.event.pull_request.head.sha }} 17 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 18 | with: 19 | go-version: stable 20 | - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 21 | id: goreleaser 22 | with: 23 | version: latest 24 | args: build --clean --verbose --single-target --snapshot 25 | check-tag: 26 | permissions: 27 | contents: read 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | with: 32 | fetch-depth: 0 33 | - id: ccv 34 | uses: smlx/ccv@7318e2f25a52dcd550e75384b84983973251a1f8 # v0.10.0 35 | with: 36 | write-tag: false 37 | - run: | 38 | echo "new-tag=$NEW_TAG" 39 | echo "new-tag-version=$NEW_TAG_VERSION" 40 | echo "new-tag-version-type=$NEW_TAG_VERSION_TYPE" 41 | env: 42 | NEW_TAG: ${{steps.ccv.outputs.new-tag}} 43 | NEW_TAG_VERSION: ${{steps.ccv.outputs.new-tag-version}} 44 | NEW_TAG_VERSION_TYPE: ${{steps.ccv.outputs.new-tag-version-type}} 45 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: {} 7 | jobs: 8 | release-tag: 9 | permissions: 10 | # create tag 11 | contents: write 12 | runs-on: ubuntu-latest 13 | outputs: 14 | new-tag: ${{ steps.ccv.outputs.new-tag }} 15 | steps: 16 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 17 | with: 18 | fetch-depth: 0 19 | - name: Bump tag if necessary 20 | id: ccv 21 | uses: smlx/ccv@7318e2f25a52dcd550e75384b84983973251a1f8 # v0.10.0 22 | release-build: 23 | permissions: 24 | # create release 25 | contents: write 26 | # push docker images to regsitry 27 | packages: write 28 | # use OIDC token for signing 29 | id-token: write 30 | # required by attest-build-provenance 31 | attestations: write 32 | needs: release-tag 33 | if: needs.release-tag.outputs.new-tag == 'true' 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 37 | with: 38 | fetch-depth: 0 39 | - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 40 | with: 41 | go-version: stable 42 | - name: Login to GHCR 43 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.GITHUB_TOKEN }} 48 | - name: Set up environment 49 | run: echo "GOVERSION=$(go version)" >> "$GITHUB_ENV" 50 | - uses: advanced-security/sbom-generator-action@6fe43abf522b2e7a19bc769aec1e6c848614b517 # v0.0.2 51 | id: sbom 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | - name: Move sbom to avoid dirty git 55 | run: mv "$GITHUB_SBOM_PATH" ./sbom.spdx.json 56 | env: 57 | GITHUB_SBOM_PATH: ${{ steps.sbom.outputs.fileName }} 58 | - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 59 | id: goreleaser 60 | with: 61 | version: latest 62 | args: release --clean 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | GITHUB_SBOM_PATH: ./sbom.spdx.json 66 | # attest archives 67 | - uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 68 | with: 69 | subject-path: "dist/*.tar.gz" 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Conventional Commits Versioner 2 | 3 | [![Release](https://github.com/smlx/ccv/actions/workflows/release.yaml/badge.svg)](https://github.com/smlx/ccv/actions/workflows/release.yaml) 4 | [![coverage](https://raw.githubusercontent.com/smlx/ccv/badges/.badges/main/coverage.svg)](https://github.com/smlx/ccv/actions/workflows/coverage.yaml) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/smlx/ccv)](https://goreportcard.com/report/github.com/smlx/ccv) 6 | 7 | `ccv` does one thing: it walks git commit history back from the current `HEAD` to find the most recent tag, taking note of commit messages along the way. 8 | When it reaches the most recent tag, it uses the commit messages it saw to figure out how the tag should be incremented, and prints the incremented tag. 9 | 10 | `ccv` is intended for use in continuous delivery automation. 11 | 12 | The ideas behind `ccv` are described by [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/). Currently parts 1 to 3 of the Conventional Commits specification summary are recognized when incrementing versions. 13 | 14 | ## Use as a Github Action 15 | 16 | This repository is also a [Github Action](https://docs.github.com/en/actions). 17 | 18 | Inputs: 19 | 20 | * `write-tag`: If true, and ccv determines that a new version is required, the action will automatically write the new version tag to the repository. Default `true`. 21 | 22 | Outputs: 23 | 24 | * `new-tag`: Either "true" or "false" depending on whether a new tag was pushed. Example: `true`. 25 | * `new-tag-version`: The new version that was tagged. This will only be set if new_tag=true. Example: `v0.1.2`. 26 | * `new-tag-version-type`: The new version type (major, minor, patch) was tagged. This will only be set if new_tag=true. Example: `minor`. 27 | 28 | ### Example: automatic tagging 29 | 30 | The main use-case of this action is to automatically tag and build new releases in a fully automated release workflow. 31 | 32 | ```yaml 33 | name: release 34 | on: 35 | push: 36 | branches: 37 | - main 38 | permissions: {} 39 | jobs: 40 | release-tag: 41 | permissions: 42 | # create tag 43 | contents: write 44 | runs-on: ubuntu-latest 45 | outputs: 46 | new-tag: ${{ steps.ccv.outputs.new-tag }} 47 | steps: 48 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | with: 50 | fetch-depth: 0 51 | - name: Bump tag if necessary 52 | id: ccv 53 | uses: smlx/ccv@7318e2f25a52dcd550e75384b84983973251a1f8 # v0.10.0 54 | release-build: 55 | permissions: 56 | # create release 57 | contents: write 58 | # push docker images to registry 59 | packages: write 60 | needs: release-tag 61 | if: needs.release-tag.outputs.new-tag == 'true' 62 | runs-on: ubuntu-latest 63 | steps: 64 | # ... build and release steps here 65 | ``` 66 | 67 | For a fully-functional example, see the [release workflow of this repository](https://github.com/smlx/ccv/blob/main/.github/workflows/release.yaml). 68 | 69 | ### Example: read-only 70 | 71 | You can also check the tag your PR will generate by running with `write-tag: false`. Note that the permissions on this job are read-only. 72 | 73 | ```yaml 74 | name: build 75 | on: 76 | pull_request: 77 | branches: 78 | - main 79 | permissions: {} 80 | jobs: 81 | check-tag: 82 | permissions: 83 | contents: read 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 87 | with: 88 | fetch-depth: 0 89 | - id: ccv 90 | uses: smlx/ccv@c5f6769c943c082c4e8d8ccf2ec4b6f5f517e1f2 # v0.7.3 91 | with: 92 | write-tag: false 93 | - run: | 94 | echo "new-tag=$NEW_TAG" 95 | echo "new-tag-version=$NEW_TAG_VERSION" 96 | echo "new-tag-version-type=$NEW_TAG_VERSION_TYPE" 97 | env: 98 | NEW_TAG: ${{steps.ccv.outputs.new-tag}} 99 | NEW_TAG_VERSION: ${{steps.ccv.outputs.new-tag-version}} 100 | NEW_TAG_VERSION_TYPE: ${{steps.ccv.outputs.new-tag-version-type}} 101 | ``` 102 | 103 | Gives this output: 104 | 105 | ``` 106 | new-tag=true 107 | new-tag-version=v0.16.0 108 | new-tag-version-type=minor 109 | ``` 110 | 111 | For a fully-functional example, see the [build workflow of this repository](https://github.com/smlx/ccv/blob/main/.github/workflows/build.yaml). 112 | 113 | ## Use locally 114 | 115 | Download the latest [release](https://github.com/smlx/ccv/releases) on github, or: 116 | 117 | ``` 118 | go install github.com/smlx/ccv/cmd/ccv@latest 119 | ``` 120 | 121 | Run `ccv` in the directory containing your git repository. 122 | 123 | ## Prior art 124 | 125 | * [caarlos0/svu](https://github.com/caarlos0/svu) does pretty much the same thing, but it has more features and shells out to git. `ccv` uses [go-git/go-git](https://github.com/go-git/go-git) instead. 126 | -------------------------------------------------------------------------------- /ccv.go: -------------------------------------------------------------------------------- 1 | // Package ccv implements the conventional commits versioner logic. 2 | package ccv 3 | 4 | import ( 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/Masterminds/semver/v3" 9 | "github.com/go-git/go-git/v5" 10 | "github.com/go-git/go-git/v5/plumbing" 11 | "github.com/go-git/go-git/v5/plumbing/object" 12 | ) 13 | 14 | var patchRegex = regexp.MustCompile(`^fix(\(.+\))?: `) 15 | var minorRegex = regexp.MustCompile(`^feat(\(.+\))?: `) 16 | var majorRegex = regexp.MustCompile(`^(fix|feat)(\(.+\))?!: |BREAKING CHANGE: `) 17 | 18 | // walkCommits walks the git history in the defined order until it reaches a 19 | // tag, analysing the commits it finds. 20 | func walkCommits(r *git.Repository, tagRefs map[string]string, order git.LogOrder) (*semver.Version, bool, bool, bool, error) { 21 | var major, minor, patch bool 22 | var stopIter = fmt.Errorf("stop commit iteration") 23 | var latestTag string 24 | // walk commit hashes back from HEAD via main 25 | commits, err := r.Log(&git.LogOptions{Order: order}) 26 | if err != nil { 27 | return nil, false, false, false, fmt.Errorf("couldn't get commits: %w", err) 28 | } 29 | err = commits.ForEach(func(c *object.Commit) error { 30 | if latestTag = tagRefs[c.Hash.String()]; latestTag != "" { 31 | return stopIter 32 | } 33 | // analyze commit message 34 | if patchRegex.MatchString(c.Message) { 35 | patch = true 36 | } 37 | if minorRegex.MatchString(c.Message) { 38 | minor = true 39 | } 40 | if majorRegex.MatchString(c.Message) { 41 | major = true 42 | } 43 | return nil 44 | }) 45 | if err != nil && err != stopIter { 46 | return nil, false, false, false, 47 | fmt.Errorf("couldn't determine latest tag: %w", err) 48 | } 49 | // not tagged yet. this can happen if we are on a branch with no tags. 50 | if latestTag == "" { 51 | return nil, false, false, false, nil 52 | } 53 | // found a tag: parse, increment, and return. 54 | latestVersion, err := semver.NewVersion(latestTag) 55 | if err != nil { 56 | return nil, false, false, false, 57 | fmt.Errorf(`couldn't parse tag "%v": %w`, latestTag, err) 58 | } 59 | return latestVersion, major, minor, patch, nil 60 | } 61 | 62 | // NextVersion returns a string containing the next version number based on the 63 | // state of the git repository in path. It inspects the most recent tag, and 64 | // the commits made after that tag. 65 | func NextVersion(path string) (string, error) { 66 | return nextVersion(path, false) 67 | } 68 | 69 | // NextVersionType returns a string containing the next version type (major, 70 | // minor, patch) based on the state of the git repository in path. It inspects 71 | // the most recent tag, and the commits made after that tag. 72 | func NextVersionType(path string) (string, error) { 73 | return nextVersion(path, true) 74 | } 75 | 76 | // nextVersion returns a string containing either the next version number, or 77 | // the next version type (major, minor, patch) based on the state of the git 78 | // repository in path. It inspects the most recent tag, and the commits made 79 | // after that tag. 80 | func nextVersion(path string, versionType bool) (string, error) { 81 | // open repository 82 | r, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true}) 83 | if err != nil { 84 | return "", fmt.Errorf("couldn't open git repository: %w", err) 85 | } 86 | tags, err := r.Tags() 87 | if err != nil { 88 | return "", fmt.Errorf("couldn't get tags: %w", err) 89 | } 90 | // map tags to commit hashes 91 | tagRefs := map[string]string{} 92 | err = tags.ForEach(func(r *plumbing.Reference) error { 93 | tagRefs[r.Hash().String()] = r.Name().Short() 94 | return nil 95 | }) 96 | if err != nil { 97 | return "", fmt.Errorf("couldn't iterate tags: %w", err) 98 | } 99 | if len(tagRefs) == 0 { 100 | // no existing tags 101 | if versionType { 102 | return "minor", nil 103 | } 104 | return "v0.1.0", nil 105 | } 106 | // now we check both main and branch to figure out what the tag should be. 107 | // this logic is required for branches which split before the latest tag on 108 | // main. See the "branch before tag and merge" test. 109 | latestMain, majorMain, minorMain, patchMain, err := 110 | walkCommits(r, tagRefs, git.LogOrderDFS) 111 | if err != nil { 112 | return "", fmt.Errorf("couldn't walk commits on main: %w", err) 113 | } 114 | latestBranch, majorBranch, minorBranch, patchBranch, err := 115 | walkCommits(r, tagRefs, git.LogOrderDFSPost) 116 | if err != nil { 117 | return "", fmt.Errorf("couldn't walk commits on branch: %w", err) 118 | } 119 | if latestMain == nil && latestBranch == nil { 120 | return "", 121 | fmt.Errorf("tags exist in the repository, but not in ancestors of HEAD") 122 | } 123 | // figure out the latest version in either parent 124 | var latestVersion *semver.Version 125 | switch { 126 | case latestMain == nil: 127 | latestVersion = latestBranch 128 | case latestBranch == nil || latestMain.GreaterThan(latestBranch): 129 | latestVersion = latestMain 130 | default: 131 | latestVersion = latestBranch 132 | } 133 | // figure out the highest increment in either parent 134 | var newVersion semver.Version 135 | var newVersionType string 136 | switch { 137 | case majorMain || majorBranch: 138 | newVersion = latestVersion.IncMajor() 139 | newVersionType = "major" 140 | case minorMain || minorBranch: 141 | newVersion = latestVersion.IncMinor() 142 | newVersionType = "minor" 143 | case patchMain || patchBranch: 144 | newVersion = latestVersion.IncPatch() 145 | newVersionType = "patch" 146 | default: 147 | newVersion = *latestVersion 148 | } 149 | if versionType { 150 | return newVersionType, nil 151 | } 152 | return fmt.Sprintf("%s%s", "v", newVersion.String()), nil 153 | } 154 | -------------------------------------------------------------------------------- /ccv_test.go: -------------------------------------------------------------------------------- 1 | package ccv_test 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "testing" 7 | 8 | "github.com/smlx/ccv" 9 | ) 10 | 11 | func TestNextVersion(t *testing.T) { 12 | var testCases = map[string]struct { 13 | gitCmds [][]string 14 | expectVersion string 15 | expectVersionType string 16 | }{ 17 | "none": {gitCmds: [][]string{ 18 | {"tag", "v0.1.0"}, 19 | {"commit", "--allow-empty", "-m", "chore: not much"}, 20 | }, 21 | expectVersion: "v0.1.0", 22 | expectVersionType: "", 23 | }, 24 | "patch": {gitCmds: [][]string{ 25 | {"tag", "v0.1.0"}, 26 | {"commit", "--allow-empty", "-m", "fix: minor bug"}, 27 | }, 28 | expectVersion: "v0.1.1", 29 | expectVersionType: "patch", 30 | }, 31 | "minor": {gitCmds: [][]string{ 32 | {"tag", "v0.1.0"}, 33 | {"commit", "--allow-empty", "-m", "feat: cool new feature"}, 34 | }, 35 | expectVersion: "v0.2.0", 36 | expectVersionType: "minor", 37 | }, 38 | "major": {gitCmds: [][]string{ 39 | {"tag", "v0.1.0"}, 40 | {"commit", "--allow-empty", "-m", "feat: major refactor\nBREAKING CHANGE: new stuff"}, 41 | }, 42 | expectVersion: "v1.0.0", 43 | expectVersionType: "major", 44 | }, 45 | "major fix bang": {gitCmds: [][]string{ 46 | {"tag", "v0.1.0"}, 47 | {"commit", "--allow-empty", "-m", "fix!: major bug"}, 48 | }, 49 | expectVersion: "v1.0.0", 50 | expectVersionType: "major", 51 | }, 52 | "major feat bang": {gitCmds: [][]string{ 53 | {"tag", "v0.1.0"}, 54 | {"commit", "--allow-empty", "-m", "feat!: major change"}, 55 | }, 56 | expectVersion: "v1.0.0", 57 | expectVersionType: "major", 58 | }, 59 | "patch with scope": {gitCmds: [][]string{ 60 | {"tag", "v0.1.0"}, 61 | {"commit", "--allow-empty", "-m", "fix(lasers): minor bug"}, 62 | }, 63 | expectVersion: "v0.1.1", 64 | expectVersionType: "patch", 65 | }, 66 | "minor with scope": {gitCmds: [][]string{ 67 | {"tag", "v0.1.0"}, 68 | {"commit", "--allow-empty", "-m", "feat(phasers): cool new feature"}, 69 | }, 70 | expectVersion: "v0.2.0", 71 | expectVersionType: "minor", 72 | }, 73 | "major with scope": {gitCmds: [][]string{ 74 | {"tag", "v0.1.0"}, 75 | {"commit", "--allow-empty", "-m", "feat(blasters): major refactor\nBREAKING CHANGE: new stuff"}, 76 | }, 77 | expectVersion: "v1.0.0", 78 | expectVersionType: "major", 79 | }, 80 | "major fix bang with scope": {gitCmds: [][]string{ 81 | {"tag", "v0.1.0"}, 82 | {"commit", "--allow-empty", "-m", "fix(lightsaber)!: major bug"}, 83 | }, 84 | expectVersion: "v1.0.0", 85 | expectVersionType: "major", 86 | }, 87 | "major feat bang with scope": {gitCmds: [][]string{ 88 | {"tag", "v0.1.0"}, 89 | {"commit", "--allow-empty", "-m", "feat(bowcaster)!: major change"}, 90 | }, 91 | expectVersion: "v1.0.0", 92 | expectVersionType: "major", 93 | }, 94 | "no existing tags feat": {gitCmds: [][]string{ 95 | {"commit", "--allow-empty", "-m", "feat: new change"}, 96 | }, 97 | expectVersion: "v0.1.0", 98 | expectVersionType: "minor", 99 | }, 100 | "no existing tags chore": {gitCmds: [][]string{ 101 | {"commit", "--allow-empty", "-m", "chore: boring change"}, 102 | }, 103 | expectVersion: "v0.1.0", 104 | expectVersionType: "minor", 105 | }, 106 | "on a branch": {gitCmds: [][]string{ 107 | {"tag", "v0.1.0"}, 108 | {"checkout", "-b", "new-branch"}, 109 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 110 | }, 111 | expectVersion: "v0.1.1", 112 | expectVersionType: "patch", 113 | }, 114 | "tag on a branch": {gitCmds: [][]string{ 115 | {"tag", "v0.1.0"}, 116 | {"checkout", "-b", "new-branch"}, 117 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 118 | {"tag", "v0.1.1"}, 119 | {"checkout", "main"}, 120 | {"commit", "--allow-empty", "-m", "feat: minor change"}, 121 | }, 122 | expectVersion: "v0.2.0", 123 | expectVersionType: "minor", 124 | }, 125 | "on a branch again": {gitCmds: [][]string{ 126 | {"tag", "v0.1.0"}, 127 | {"checkout", "-b", "new-branch"}, 128 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 129 | {"tag", "v0.1.1"}, 130 | {"checkout", "main"}, 131 | {"commit", "--allow-empty", "-m", "feat: minor change"}, 132 | {"tag", "v0.2.0"}, 133 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 134 | }, 135 | expectVersion: "v0.2.1", 136 | expectVersionType: "patch", 137 | }, 138 | "back on a branch": {gitCmds: [][]string{ 139 | {"tag", "v0.1.0"}, 140 | {"checkout", "-b", "new-branch"}, 141 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 142 | {"tag", "v0.1.1"}, 143 | {"checkout", "main"}, 144 | {"commit", "--allow-empty", "-m", "feat: minor change"}, 145 | {"tag", "v0.2.0"}, 146 | {"checkout", "new-branch"}, 147 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 148 | }, 149 | expectVersion: "v0.1.2", 150 | expectVersionType: "patch", 151 | }, 152 | "main after merge": {gitCmds: [][]string{ 153 | {"tag", "v0.1.0"}, 154 | {"checkout", "-b", "new-branch"}, 155 | {"commit", "--allow-empty", "-m", "chore: boring change"}, 156 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 157 | {"commit", "--allow-empty", "-m", "chore: boring change"}, 158 | {"checkout", "main"}, 159 | {"merge", "--no-ff", "new-branch", "-m", "chore: merge"}, 160 | }, 161 | expectVersion: "v0.1.1", 162 | expectVersionType: "patch", 163 | }, 164 | "branch after merge": {gitCmds: [][]string{ 165 | {"tag", "v0.1.0"}, 166 | {"checkout", "-b", "new-branch"}, 167 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 168 | {"checkout", "main"}, 169 | {"merge", "--no-ff", "new-branch", "-m", "chore: merge"}, 170 | {"tag", "v0.1.2"}, 171 | {"checkout", "-b", "new-branch-2"}, 172 | {"commit", "--allow-empty", "-m", "feat: major change"}, 173 | }, 174 | expectVersion: "v0.2.0", 175 | expectVersionType: "minor", 176 | }, 177 | "main after merge again": {gitCmds: [][]string{ 178 | {"tag", "v0.1.0"}, 179 | {"checkout", "-b", "new-branch"}, 180 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 181 | {"checkout", "main"}, 182 | {"merge", "--no-ff", "new-branch", "-m", "chore: merge"}, 183 | {"tag", "v0.1.2"}, 184 | {"checkout", "-b", "new-branch-2"}, 185 | {"commit", "--allow-empty", "-m", "feat: major change"}, 186 | {"checkout", "main"}, 187 | {"merge", "--no-ff", "new-branch-2", "-m", "chore: merge"}, 188 | }, 189 | expectVersion: "v0.2.0", 190 | expectVersionType: "minor", 191 | }, 192 | "branch before tag and merge": {gitCmds: [][]string{ 193 | {"tag", "v0.1.0"}, 194 | {"checkout", "-b", "new-branch-1"}, 195 | {"checkout", "-b", "new-branch-2"}, 196 | {"commit", "--allow-empty", "-m", "fix: minor change"}, 197 | {"checkout", "main"}, 198 | {"merge", "--no-ff", "new-branch-2", "-m", "chore: merge"}, 199 | {"tag", "v0.1.1"}, 200 | {"checkout", "new-branch-1"}, 201 | {"commit", "--allow-empty", "-m", "fix: another minor change"}, 202 | {"checkout", "main"}, 203 | {"merge", "--no-ff", "new-branch-1", "-m", "chore: merge"}, 204 | }, 205 | expectVersion: "v0.1.2", 206 | expectVersionType: "patch", 207 | }, 208 | } 209 | for name, tc := range testCases { 210 | t.Run(name, func(tt *testing.T) { 211 | // create test dir 212 | dir, err := os.MkdirTemp("", "example") 213 | if err != nil { 214 | tt.Fatalf("couldn't get a tempdir: %v", err) 215 | } 216 | // init git repo 217 | initCmds := [][]string{ 218 | {"init", "-b", "main"}, 219 | {"commit", "--allow-empty", "-m", "feat: initial commit"}, 220 | } 221 | for _, c := range initCmds { 222 | cmd := exec.Command("git", c...) 223 | cmd.Dir = dir 224 | if output, err := cmd.CombinedOutput(); err != nil { 225 | tt.Fatalf("couldn't run init cmd git %v: %v (%s)", c, err, output) 226 | } 227 | } 228 | for _, c := range tc.gitCmds { 229 | cmd := exec.Command("git", c...) 230 | cmd.Dir = dir 231 | if output, err := cmd.CombinedOutput(); err != nil { 232 | tt.Fatalf("couldn't run git %v: %v (%s)", c, err, output) 233 | } 234 | } 235 | next, err := ccv.NextVersion(dir) 236 | if err != nil { 237 | tt.Fatalf("error from main.NextVersion(): %v", err) 238 | } 239 | if next != tc.expectVersion { 240 | tt.Fatalf("expected: %v, got: %v", tc.expectVersion, next) 241 | } 242 | nextType, err := ccv.NextVersionType(dir) 243 | if err != nil { 244 | tt.Fatalf("error from main.NextVersionType(): %v", err) 245 | } 246 | if nextType != tc.expectVersionType { 247 | tt.Fatalf("expected: %v, got: %v", tc.expectVersionType, nextType) 248 | } 249 | }) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 4 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 5 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 6 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 7 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 8 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= 9 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 10 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 11 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 12 | github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= 13 | github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= 14 | github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= 15 | github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 16 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 17 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 18 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 19 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 20 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 21 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 22 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 23 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 24 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 28 | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 29 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 30 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 31 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 32 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 33 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 35 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 36 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 37 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 39 | github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= 40 | github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= 41 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 42 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 43 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 44 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 45 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 46 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 47 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 48 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 49 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 50 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 53 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 57 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 58 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 59 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 60 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 61 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 62 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 63 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 65 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 66 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 67 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 68 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 69 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 70 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 71 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 72 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 73 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 75 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 76 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 77 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 78 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 79 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 80 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 81 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 82 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 83 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 84 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 85 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 86 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 87 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 88 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 94 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 95 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 96 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 97 | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 98 | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 99 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 101 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 105 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 106 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 107 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 108 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 109 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 110 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 111 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 112 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------