├── .codecov.yml ├── .conform.yaml ├── .dockerignore ├── .github ├── renovate.json └── workflows │ ├── ci.yaml │ └── slack-notify.yaml ├── .gitignore ├── .golangci.yml ├── .kres.yaml ├── .license-header.go.txt ├── .markdownlint.json ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── cmd └── conform │ ├── enforce.go │ ├── main.go │ ├── root.go │ ├── serve.go │ └── version.go ├── go.mod ├── go.sum ├── hack ├── release.sh └── release.toml └── internal ├── constants └── constants.go ├── enforcer └── enforcer.go ├── git └── git.go ├── policy ├── commit │ ├── check_body.go │ ├── check_conventional_commit.go │ ├── check_dco.go │ ├── check_gpg_identity.go │ ├── check_gpg_signature.go │ ├── check_header_case.go │ ├── check_header_last_character.go │ ├── check_header_length.go │ ├── check_imperative_verb.go │ ├── check_jira.go │ ├── check_jira_test.go │ ├── check_number_of_commits.go │ ├── check_spelling.go │ ├── commit.go │ └── commit_test.go ├── license │ ├── license.go │ ├── license_test.go │ └── testdata │ │ ├── data.txt │ │ └── subdir1 │ │ ├── data.txt │ │ └── subdir2 │ │ └── data.txt ├── policy.go ├── policy_options.go └── version │ └── version.go ├── reporter └── reporter.go └── version ├── data ├── sha └── tag └── version.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-09-01T21:02:33Z by kres d88b53b-dirty. 4 | 5 | codecov: 6 | require_ci_to_pass: false 7 | 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | target: 50% 13 | threshold: 0.5% 14 | base: auto 15 | if_ci_failed: success 16 | patch: off 17 | 18 | comment: false 19 | -------------------------------------------------------------------------------- /.conform.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-03-01T05:36:30Z by kres latest. 4 | 5 | policies: 6 | - type: commit 7 | spec: 8 | dco: true 9 | gpg: 10 | required: true 11 | identity: 12 | gitHubOrganization: siderolabs 13 | spellcheck: 14 | locale: US 15 | maximumOfOneCommit: true 16 | header: 17 | length: 89 18 | imperative: true 19 | case: lower 20 | invalidLastCharacters: . 21 | body: 22 | required: true 23 | conventional: 24 | types: 25 | - chore 26 | - docs 27 | - perf 28 | - refactor 29 | - style 30 | - test 31 | - release 32 | scopes: 33 | - .* 34 | - type: license 35 | spec: 36 | root: . 37 | skipPaths: 38 | - .git/ 39 | - testdata/ 40 | includeSuffixes: 41 | - .go 42 | excludeSuffixes: 43 | - .pb.go 44 | - .pb.gw.go 45 | header: | 46 | // This Source Code Form is subject to the terms of the Mozilla Public 47 | // License, v. 2.0. If a copy of the MPL was not distributed with this 48 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 49 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-02-15T10:41:04Z by kres latest. 4 | 5 | * 6 | !cmd 7 | !internal 8 | !go.mod 9 | !go.sum 10 | !.golangci.yml 11 | !CHANGELOG.md 12 | !README.md 13 | !.markdownlint.json 14 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | ":dependencyDashboard", 5 | ":gitSignOff", 6 | ":semanticCommitScopeDisabled", 7 | "schedule:earlyMondays" 8 | ], 9 | "prHeader": "Update Request | Renovate Bot", 10 | "packageRules": [ 11 | { 12 | "matchPackageNames": [ 13 | "golang/go" 14 | ], 15 | "versioning": "regex:^(?\\d+)\\.(?\\d+)\\.?(?\\d+)?$" 16 | }, 17 | { 18 | "matchPackagePatterns": [ 19 | "*" 20 | ], 21 | "matchDatasources": [ 22 | "docker" 23 | ], 24 | "groupName": "container images" 25 | }, 26 | { 27 | "matchPackagePatterns": [ 28 | "*" 29 | ], 30 | "matchDatasources": [ 31 | "go", 32 | "golang-version" 33 | ], 34 | "groupName": "go packages" 35 | }, 36 | { 37 | "matchPackagePatterns": [ 38 | "*" 39 | ], 40 | "matchDatasources": [ 41 | "npm" 42 | ], 43 | "groupName": "node packages" 44 | }, 45 | { 46 | "matchPackagePatterns": [ 47 | "*" 48 | ], 49 | "matchDatasources": [ 50 | "git-refs", 51 | "git-tags", 52 | "github-tags", 53 | "github-releases" 54 | ], 55 | "groupName": "releases" 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-10-10T13:04:30Z by kres 34e72ac. 4 | 5 | name: default 6 | concurrency: 7 | group: ${{ github.head_ref || github.run_id }} 8 | cancel-in-progress: true 9 | "on": 10 | push: 11 | branches: 12 | - main 13 | - release-* 14 | tags: 15 | - v* 16 | pull_request: 17 | branches: 18 | - main 19 | - release-* 20 | jobs: 21 | default: 22 | permissions: 23 | actions: read 24 | contents: write 25 | issues: read 26 | packages: write 27 | pull-requests: read 28 | runs-on: 29 | - self-hosted 30 | - generic 31 | if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/')) 32 | steps: 33 | - name: gather-system-info 34 | id: system-info 35 | uses: kenchan0130/actions-system-info@v1.3.0 36 | continue-on-error: true 37 | - name: print-system-info 38 | run: | 39 | MEMORY_GB=$((${{ steps.system-info.outputs.totalmem }}/1024/1024/1024)) 40 | 41 | OUTPUTS=( 42 | "CPU Core: ${{ steps.system-info.outputs.cpu-core }}" 43 | "CPU Model: ${{ steps.system-info.outputs.cpu-model }}" 44 | "Hostname: ${{ steps.system-info.outputs.hostname }}" 45 | "NodeName: ${NODE_NAME}" 46 | "Kernel release: ${{ steps.system-info.outputs.kernel-release }}" 47 | "Kernel version: ${{ steps.system-info.outputs.kernel-version }}" 48 | "Name: ${{ steps.system-info.outputs.name }}" 49 | "Platform: ${{ steps.system-info.outputs.platform }}" 50 | "Release: ${{ steps.system-info.outputs.release }}" 51 | "Total memory: ${MEMORY_GB} GB" 52 | ) 53 | 54 | for OUTPUT in "${OUTPUTS[@]}";do 55 | echo "${OUTPUT}" 56 | done 57 | continue-on-error: true 58 | - name: checkout 59 | uses: actions/checkout@v4 60 | - name: Unshallow 61 | run: | 62 | git fetch --prune --unshallow 63 | - name: Set up Docker Buildx 64 | id: setup-buildx 65 | uses: docker/setup-buildx-action@v3 66 | with: 67 | driver: remote 68 | endpoint: tcp://buildkit-amd64.ci.svc.cluster.local:1234 69 | timeout-minutes: 10 70 | - name: base 71 | run: | 72 | make base 73 | - name: unit-tests 74 | run: | 75 | make unit-tests 76 | - name: unit-tests-race 77 | run: | 78 | make unit-tests-race 79 | - name: coverage 80 | uses: codecov/codecov-action@v4 81 | with: 82 | files: _out/coverage-unit-tests.txt 83 | token: ${{ secrets.CODECOV_TOKEN }} 84 | timeout-minutes: 3 85 | - name: conform 86 | run: | 87 | make conform 88 | - name: lint 89 | run: | 90 | make lint 91 | - name: Login to registry 92 | if: github.event_name != 'pull_request' 93 | uses: docker/login-action@v3 94 | with: 95 | password: ${{ secrets.GITHUB_TOKEN }} 96 | registry: ghcr.io 97 | username: ${{ github.repository_owner }} 98 | - name: image-conform 99 | run: | 100 | make image-conform 101 | - name: push-conform 102 | if: github.event_name != 'pull_request' 103 | env: 104 | PLATFORM: linux/amd64,linux/arm64 105 | PUSH: "true" 106 | run: | 107 | make image-conform 108 | - name: push-conform-latest 109 | if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' 110 | env: 111 | PLATFORM: linux/amd64,linux/arm64 112 | PUSH: "true" 113 | run: | 114 | make image-conform IMAGE_TAG=latest 115 | - name: Generate Checksums 116 | if: startsWith(github.ref, 'refs/tags/') 117 | run: | 118 | cd _out 119 | sha256sum conform-* > sha256sum.txt 120 | sha512sum conform-* > sha512sum.txt 121 | - name: release-notes 122 | if: startsWith(github.ref, 'refs/tags/') 123 | run: | 124 | make release-notes 125 | - name: Release 126 | if: startsWith(github.ref, 'refs/tags/') 127 | uses: crazy-max/ghaction-github-release@v2 128 | with: 129 | body_path: _out/RELEASE_NOTES.md 130 | draft: "true" 131 | files: |- 132 | _out/conform-* 133 | _out/sha*.txt 134 | -------------------------------------------------------------------------------- /.github/workflows/slack-notify.yaml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-02-15T10:41:04Z by kres latest. 4 | 5 | name: slack-notify 6 | "on": 7 | workflow_run: 8 | workflows: 9 | - default 10 | types: 11 | - completed 12 | jobs: 13 | slack-notify: 14 | runs-on: 15 | - self-hosted 16 | - generic 17 | if: github.event.workflow_run.conclusion != 'skipped' 18 | steps: 19 | - name: Get PR number 20 | id: get-pr-number 21 | if: github.event.workflow_run.event == 'pull_request' 22 | env: 23 | GH_TOKEN: ${{ github.token }} 24 | run: | 25 | echo pull_request_number=$(gh pr view -R ${{ github.repository }} ${{ github.event.workflow_run.head_repository.owner.login }}:${{ github.event.workflow_run.head_branch }} --json number --jq .number) >> $GITHUB_OUTPUT 26 | - name: Slack Notify 27 | uses: slackapi/slack-github-action@v1 28 | with: 29 | channel-id: proj-talos-maintainers 30 | payload: | 31 | { 32 | "attachments": [ 33 | { 34 | "color": "${{ github.event.workflow_run.conclusion == 'success' && '#2EB886' || github.event.workflow_run.conclusion == 'failure' && '#A30002' || '#FFCC00' }}", 35 | "fallback": "test", 36 | "blocks": [ 37 | { 38 | "type": "section", 39 | "fields": [ 40 | { 41 | "type": "mrkdwn", 42 | "text": "${{ github.event.workflow_run.event == 'pull_request' && format('*Pull Request:* {0} (`{1}`)\n<{2}/pull/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, steps.get-pr-number.outputs.pull_request_number, github.event.workflow_run.display_title) || format('*Build:* {0} (`{1}`)\n<{2}/commit/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, github.sha, github.event.workflow_run.display_title) }}" 43 | }, 44 | { 45 | "type": "mrkdwn", 46 | "text": "*Status:*\n`${{ github.event.workflow_run.conclusion }}`" 47 | } 48 | ] 49 | }, 50 | { 51 | "type": "section", 52 | "fields": [ 53 | { 54 | "type": "mrkdwn", 55 | "text": "*Author:*\n`${{ github.actor }}`" 56 | }, 57 | { 58 | "type": "mrkdwn", 59 | "text": "*Event:*\n`${{ github.event.workflow_run.event }}`" 60 | } 61 | ] 62 | }, 63 | { 64 | "type": "divider" 65 | }, 66 | { 67 | "type": "actions", 68 | "elements": [ 69 | { 70 | "type": "button", 71 | "text": { 72 | "type": "plain_text", 73 | "text": "Logs" 74 | }, 75 | "url": "${{ github.event.workflow_run.html_url }}" 76 | }, 77 | { 78 | "type": "button", 79 | "text": { 80 | "type": "plain_text", 81 | "text": "Commit" 82 | }, 83 | "url": "${{ github.event.repository.html_url }}/commit/${{ github.sha }}" 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | env: 92 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-09-01T21:02:33Z by kres d88b53b-dirty. 4 | 5 | _out 6 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-10-10T13:02:45Z by kres 34e72ac. 4 | 5 | # options for analysis running 6 | run: 7 | timeout: 10m 8 | issues-exit-code: 1 9 | tests: true 10 | build-tags: [ ] 11 | modules-download-mode: readonly 12 | 13 | # output configuration options 14 | output: 15 | formats: 16 | - format: colored-line-number 17 | path: stdout 18 | print-issued-lines: true 19 | print-linter-name: true 20 | uniq-by-line: true 21 | path-prefix: "" 22 | 23 | # all available settings of specific linters 24 | linters-settings: 25 | dogsled: 26 | max-blank-identifiers: 2 27 | dupl: 28 | threshold: 150 29 | errcheck: 30 | check-type-assertions: true 31 | check-blank: true 32 | exhaustive: 33 | default-signifies-exhaustive: false 34 | gci: 35 | sections: 36 | - standard # Standard section: captures all standard packages. 37 | - default # Default section: contains all imports that could not be matched to another section type. 38 | - localmodule # Imports from the same module. 39 | gocognit: 40 | min-complexity: 30 41 | nestif: 42 | min-complexity: 5 43 | goconst: 44 | min-len: 3 45 | min-occurrences: 3 46 | gocritic: 47 | disabled-checks: [ ] 48 | gocyclo: 49 | min-complexity: 20 50 | godot: 51 | scope: declarations 52 | gofmt: 53 | simplify: true 54 | gomodguard: { } 55 | govet: 56 | enable-all: true 57 | lll: 58 | line-length: 200 59 | tab-width: 4 60 | misspell: 61 | locale: US 62 | ignore-words: [ ] 63 | nakedret: 64 | max-func-lines: 30 65 | prealloc: 66 | simple: true 67 | range-loops: true # Report preallocation suggestions on range loops, true by default 68 | for-loops: false # Report preallocation suggestions on for loops, false by default 69 | nolintlint: 70 | allow-unused: false 71 | allow-no-explanation: [ ] 72 | require-explanation: false 73 | require-specific: true 74 | rowserrcheck: { } 75 | testpackage: { } 76 | unparam: 77 | check-exported: false 78 | unused: 79 | local-variables-are-used: false 80 | whitespace: 81 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 82 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 83 | wsl: 84 | strict-append: true 85 | allow-assign-and-call: true 86 | allow-multiline-assign: true 87 | allow-cuddle-declarations: false 88 | allow-trailing-comment: false 89 | force-case-trailing-whitespace: 0 90 | force-err-cuddling: false 91 | allow-separated-leading-comment: false 92 | gofumpt: 93 | extra-rules: false 94 | cyclop: 95 | # the maximal code complexity to report 96 | max-complexity: 20 97 | depguard: 98 | rules: 99 | prevent_unmaintained_packages: 100 | list-mode: lax # allow unless explicitly denied 101 | files: 102 | - $all 103 | deny: 104 | - pkg: io/ioutil 105 | desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil" 106 | 107 | linters: 108 | enable-all: true 109 | disable-all: false 110 | fast: false 111 | disable: 112 | - exhaustruct 113 | - err113 114 | - forbidigo 115 | - funlen 116 | - gochecknoglobals 117 | - gochecknoinits 118 | - godox 119 | - gomnd 120 | - gomoddirectives 121 | - gosec 122 | - inamedparam 123 | - ireturn 124 | - mnd 125 | - nestif 126 | - nonamedreturns 127 | - paralleltest 128 | - tagalign 129 | - tagliatelle 130 | - thelper 131 | - varnamelen 132 | - wrapcheck 133 | - testifylint # complains about our assert recorder and has a number of false positives for assert.Greater(t, thing, 1) 134 | - protogetter # complains about us using Value field on typed spec, instead of GetValue which has a different signature 135 | - perfsprint # complains about us using fmt.Sprintf in non-performance critical code, updating just kres took too long 136 | - goimports # same as gci 137 | - musttag # seems to be broken - goes into imported libraries and reports issues there 138 | 139 | issues: 140 | exclude: [ ] 141 | exclude-rules: [ ] 142 | exclude-use-default: false 143 | exclude-case-sensitive: false 144 | max-issues-per-linter: 10 145 | max-same-issues: 3 146 | new: false 147 | 148 | severity: 149 | default-severity: error 150 | case-sensitive: false 151 | -------------------------------------------------------------------------------- /.kres.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: common.Image 3 | name: image-conform 4 | spec: 5 | extraEnvironment: 6 | PLATFORM: linux/amd64,linux/arm64 7 | --- 8 | kind: golang.Build 9 | spec: 10 | outputs: 11 | linux-amd64: 12 | GOOS: linux 13 | GOARCH: amd64 14 | linux-arm64: 15 | GOOS: linux 16 | GOARCH: arm64 17 | darwin-amd64: 18 | GOOS: darwin 19 | GOARCH: amd64 20 | darwin-arm64: 21 | GOOS: darwin 22 | GOARCH: arm64 23 | --- 24 | kind: golang.Toolchain 25 | spec: 26 | extraPackages: 27 | - git 28 | --- 29 | kind: golang.Generate 30 | spec: 31 | versionPackagePath: internal/version 32 | -------------------------------------------------------------------------------- /.license-header.go.txt: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2021-09-01T21:02:33Z by kres d88b53b-dirty. 4 | 5 | { 6 | "MD013": false, 7 | "MD033": false, 8 | "default": true 9 | } 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # install with `pre-commit install -t commit-msg` 3 | repos: 4 | - repo: https://github.com/siderolabs/conform 5 | rev: main 6 | hooks: 7 | - id: conform 8 | stages: 9 | - commit-msg 10 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: conform 3 | name: Conform 4 | description: Run 'conform enforce' for policy enforcement 5 | entry: conform enforce --commit-msg-file 6 | language: golang 7 | stages: [commit-msg] 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile-upstream:1.10.0-labs 2 | 3 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 4 | # 5 | # Generated on 2024-10-10T13:02:45Z by kres 34e72ac. 6 | 7 | ARG TOOLCHAIN 8 | 9 | FROM ghcr.io/siderolabs/ca-certificates:v1.8.0 AS image-ca-certificates 10 | 11 | FROM ghcr.io/siderolabs/fhs:v1.8.0 AS image-fhs 12 | 13 | # runs markdownlint 14 | FROM docker.io/oven/bun:1.1.29-alpine AS lint-markdown 15 | WORKDIR /src 16 | RUN bun i markdownlint-cli@0.41.0 sentences-per-line@0.2.1 17 | COPY .markdownlint.json . 18 | COPY ./CHANGELOG.md ./CHANGELOG.md 19 | COPY ./README.md ./README.md 20 | RUN bunx markdownlint --ignore "CHANGELOG.md" --ignore "**/node_modules/**" --ignore '**/hack/chglog/**' --rules node_modules/sentences-per-line/index.js . 21 | 22 | # base toolchain image 23 | FROM --platform=${BUILDPLATFORM} ${TOOLCHAIN} AS toolchain 24 | RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev git 25 | 26 | # build tools 27 | FROM --platform=${BUILDPLATFORM} toolchain AS tools 28 | ENV GO111MODULE=on 29 | ARG CGO_ENABLED 30 | ENV CGO_ENABLED=${CGO_ENABLED} 31 | ARG GOTOOLCHAIN 32 | ENV GOTOOLCHAIN=${GOTOOLCHAIN} 33 | ARG GOEXPERIMENT 34 | ENV GOEXPERIMENT=${GOEXPERIMENT} 35 | ENV GOPATH=/go 36 | ARG DEEPCOPY_VERSION 37 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \ 38 | && mv /go/bin/deep-copy /bin/deep-copy 39 | ARG GOLANGCILINT_VERSION 40 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \ 41 | && mv /go/bin/golangci-lint /bin/golangci-lint 42 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/vuln/cmd/govulncheck@latest \ 43 | && mv /go/bin/govulncheck /bin/govulncheck 44 | ARG GOFUMPT_VERSION 45 | RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \ 46 | && mv /go/bin/gofumpt /bin/gofumpt 47 | 48 | # tools and sources 49 | FROM tools AS base 50 | WORKDIR /src 51 | COPY go.mod go.mod 52 | COPY go.sum go.sum 53 | RUN cd . 54 | RUN --mount=type=cache,target=/go/pkg go mod download 55 | RUN --mount=type=cache,target=/go/pkg go mod verify 56 | COPY ./cmd ./cmd 57 | COPY ./internal ./internal 58 | RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null 59 | 60 | FROM tools AS embed-generate 61 | ARG SHA 62 | ARG TAG 63 | WORKDIR /src 64 | RUN mkdir -p internal/version/data && \ 65 | echo -n ${SHA} > internal/version/data/sha && \ 66 | echo -n ${TAG} > internal/version/data/tag 67 | 68 | # runs gofumpt 69 | FROM base AS lint-gofumpt 70 | RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w .':\n${FILES}"; exit 1) 71 | 72 | # runs golangci-lint 73 | FROM base AS lint-golangci-lint 74 | WORKDIR /src 75 | COPY .golangci.yml . 76 | ENV GOGC=50 77 | RUN golangci-lint config verify --config .golangci.yml 78 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/.cache/golangci-lint --mount=type=cache,target=/go/pkg golangci-lint run --config .golangci.yml 79 | 80 | # runs govulncheck 81 | FROM base AS lint-govulncheck 82 | WORKDIR /src 83 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg govulncheck ./... 84 | 85 | # runs unit-tests with race detector 86 | FROM base AS unit-tests-race 87 | WORKDIR /src 88 | ARG TESTPKGS 89 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp CGO_ENABLED=1 go test -v -race -count 1 ${TESTPKGS} 90 | 91 | # runs unit-tests 92 | FROM base AS unit-tests-run 93 | WORKDIR /src 94 | ARG TESTPKGS 95 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS} 96 | 97 | FROM embed-generate AS embed-abbrev-generate 98 | WORKDIR /src 99 | ARG ABBREV_TAG 100 | RUN echo -n 'undefined' > internal/version/data/sha && \ 101 | echo -n ${ABBREV_TAG} > internal/version/data/tag 102 | 103 | FROM scratch AS unit-tests 104 | COPY --from=unit-tests-run /src/coverage.txt /coverage-unit-tests.txt 105 | 106 | # cleaned up specs and compiled versions 107 | FROM scratch AS generate 108 | COPY --from=embed-abbrev-generate /src/internal/version internal/version 109 | 110 | # builds conform-darwin-amd64 111 | FROM base AS conform-darwin-amd64-build 112 | COPY --from=generate / / 113 | COPY --from=embed-generate / / 114 | WORKDIR /src/cmd/conform 115 | ARG GO_BUILDFLAGS 116 | ARG GO_LDFLAGS 117 | ARG VERSION_PKG="internal/version" 118 | ARG SHA 119 | ARG TAG 120 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=amd64 GOOS=darwin go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=conform -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /conform-darwin-amd64 121 | 122 | # builds conform-darwin-arm64 123 | FROM base AS conform-darwin-arm64-build 124 | COPY --from=generate / / 125 | COPY --from=embed-generate / / 126 | WORKDIR /src/cmd/conform 127 | ARG GO_BUILDFLAGS 128 | ARG GO_LDFLAGS 129 | ARG VERSION_PKG="internal/version" 130 | ARG SHA 131 | ARG TAG 132 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=arm64 GOOS=darwin go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=conform -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /conform-darwin-arm64 133 | 134 | # builds conform-linux-amd64 135 | FROM base AS conform-linux-amd64-build 136 | COPY --from=generate / / 137 | COPY --from=embed-generate / / 138 | WORKDIR /src/cmd/conform 139 | ARG GO_BUILDFLAGS 140 | ARG GO_LDFLAGS 141 | ARG VERSION_PKG="internal/version" 142 | ARG SHA 143 | ARG TAG 144 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=amd64 GOOS=linux go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=conform -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /conform-linux-amd64 145 | 146 | # builds conform-linux-arm64 147 | FROM base AS conform-linux-arm64-build 148 | COPY --from=generate / / 149 | COPY --from=embed-generate / / 150 | WORKDIR /src/cmd/conform 151 | ARG GO_BUILDFLAGS 152 | ARG GO_LDFLAGS 153 | ARG VERSION_PKG="internal/version" 154 | ARG SHA 155 | ARG TAG 156 | RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=arm64 GOOS=linux go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=conform -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /conform-linux-arm64 157 | 158 | FROM scratch AS conform-darwin-amd64 159 | COPY --from=conform-darwin-amd64-build /conform-darwin-amd64 /conform-darwin-amd64 160 | 161 | FROM scratch AS conform-darwin-arm64 162 | COPY --from=conform-darwin-arm64-build /conform-darwin-arm64 /conform-darwin-arm64 163 | 164 | FROM scratch AS conform-linux-amd64 165 | COPY --from=conform-linux-amd64-build /conform-linux-amd64 /conform-linux-amd64 166 | 167 | FROM scratch AS conform-linux-arm64 168 | COPY --from=conform-linux-arm64-build /conform-linux-arm64 /conform-linux-arm64 169 | 170 | FROM conform-linux-${TARGETARCH} AS conform 171 | 172 | FROM scratch AS conform-all 173 | COPY --from=conform-darwin-amd64 / / 174 | COPY --from=conform-darwin-arm64 / / 175 | COPY --from=conform-linux-amd64 / / 176 | COPY --from=conform-linux-arm64 / / 177 | 178 | FROM scratch AS image-conform 179 | ARG TARGETARCH 180 | COPY --from=conform conform-linux-${TARGETARCH} /conform 181 | COPY --from=image-fhs / / 182 | COPY --from=image-ca-certificates / / 183 | LABEL org.opencontainers.image.source=https://github.com/siderolabs/conform 184 | ENTRYPOINT ["/conform"] 185 | 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 2 | # 3 | # Generated on 2024-10-10T13:02:45Z by kres 34e72ac. 4 | 5 | # common variables 6 | 7 | SHA := $(shell git describe --match=none --always --abbrev=8 --dirty) 8 | TAG := $(shell git describe --tag --always --dirty --match v[0-9]\*) 9 | ABBREV_TAG := $(shell git describe --tags >/dev/null 2>/dev/null && git describe --tag --always --match v[0-9]\* --abbrev=0 || echo 'undefined') 10 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 11 | ARTIFACTS := _out 12 | IMAGE_TAG ?= $(TAG) 13 | OPERATING_SYSTEM := $(shell uname -s | tr '[:upper:]' '[:lower:]') 14 | GOARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/') 15 | WITH_DEBUG ?= false 16 | WITH_RACE ?= false 17 | REGISTRY ?= ghcr.io 18 | USERNAME ?= siderolabs 19 | REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) 20 | PROTOBUF_GO_VERSION ?= 1.34.2 21 | GRPC_GO_VERSION ?= 1.5.1 22 | GRPC_GATEWAY_VERSION ?= 2.22.0 23 | VTPROTOBUF_VERSION ?= 0.6.0 24 | GOIMPORTS_VERSION ?= 0.25.0 25 | DEEPCOPY_VERSION ?= v0.5.6 26 | GOLANGCILINT_VERSION ?= v1.61.0 27 | GOFUMPT_VERSION ?= v0.7.0 28 | GO_VERSION ?= 1.23.2 29 | GO_BUILDFLAGS ?= 30 | GO_LDFLAGS ?= 31 | CGO_ENABLED ?= 0 32 | GOTOOLCHAIN ?= local 33 | TESTPKGS ?= ./... 34 | KRES_IMAGE ?= ghcr.io/siderolabs/kres:latest 35 | CONFORMANCE_IMAGE ?= ghcr.io/siderolabs/conform:latest 36 | 37 | # docker build settings 38 | 39 | BUILD := docker buildx build 40 | PLATFORM ?= linux/amd64 41 | PROGRESS ?= auto 42 | PUSH ?= false 43 | CI_ARGS ?= 44 | COMMON_ARGS = --file=Dockerfile 45 | COMMON_ARGS += --provenance=false 46 | COMMON_ARGS += --progress=$(PROGRESS) 47 | COMMON_ARGS += --platform=$(PLATFORM) 48 | COMMON_ARGS += --push=$(PUSH) 49 | COMMON_ARGS += --build-arg=ARTIFACTS="$(ARTIFACTS)" 50 | COMMON_ARGS += --build-arg=SHA="$(SHA)" 51 | COMMON_ARGS += --build-arg=TAG="$(TAG)" 52 | COMMON_ARGS += --build-arg=ABBREV_TAG="$(ABBREV_TAG)" 53 | COMMON_ARGS += --build-arg=USERNAME="$(USERNAME)" 54 | COMMON_ARGS += --build-arg=REGISTRY="$(REGISTRY)" 55 | COMMON_ARGS += --build-arg=TOOLCHAIN="$(TOOLCHAIN)" 56 | COMMON_ARGS += --build-arg=CGO_ENABLED="$(CGO_ENABLED)" 57 | COMMON_ARGS += --build-arg=GO_BUILDFLAGS="$(GO_BUILDFLAGS)" 58 | COMMON_ARGS += --build-arg=GO_LDFLAGS="$(GO_LDFLAGS)" 59 | COMMON_ARGS += --build-arg=GOTOOLCHAIN="$(GOTOOLCHAIN)" 60 | COMMON_ARGS += --build-arg=GOEXPERIMENT="$(GOEXPERIMENT)" 61 | COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION="$(PROTOBUF_GO_VERSION)" 62 | COMMON_ARGS += --build-arg=GRPC_GO_VERSION="$(GRPC_GO_VERSION)" 63 | COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION="$(GRPC_GATEWAY_VERSION)" 64 | COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION="$(VTPROTOBUF_VERSION)" 65 | COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)" 66 | COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)" 67 | COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" 68 | COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" 69 | COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)" 70 | TOOLCHAIN ?= docker.io/golang:1.23-alpine 71 | 72 | # help menu 73 | 74 | export define HELP_MENU_HEADER 75 | # Getting Started 76 | 77 | To build this project, you must have the following installed: 78 | 79 | - git 80 | - make 81 | - docker (19.03 or higher) 82 | 83 | ## Creating a Builder Instance 84 | 85 | The build process makes use of experimental Docker features (buildx). 86 | To enable experimental features, add 'experimental: "true"' to '/etc/docker/daemon.json' on 87 | Linux or enable experimental features in Docker GUI for Windows or Mac. 88 | 89 | To create a builder instance, run: 90 | 91 | docker buildx create --name local --use 92 | 93 | If running builds that needs to be cached aggresively create a builder instance with the following: 94 | 95 | docker buildx create --name local --use --config=config.toml 96 | 97 | config.toml contents: 98 | 99 | [worker.oci] 100 | gc = true 101 | gckeepstorage = 50000 102 | 103 | [[worker.oci.gcpolicy]] 104 | keepBytes = 10737418240 105 | keepDuration = 604800 106 | filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"] 107 | [[worker.oci.gcpolicy]] 108 | all = true 109 | keepBytes = 53687091200 110 | 111 | If you already have a compatible builder instance, you may use that instead. 112 | 113 | ## Artifacts 114 | 115 | All artifacts will be output to ./$(ARTIFACTS). Images will be tagged with the 116 | registry "$(REGISTRY)", username "$(USERNAME)", and a dynamic tag (e.g. $(IMAGE):$(IMAGE_TAG)). 117 | The registry and username can be overridden by exporting REGISTRY, and USERNAME 118 | respectively. 119 | 120 | endef 121 | 122 | ifneq (, $(filter $(WITH_RACE), t true TRUE y yes 1)) 123 | GO_BUILDFLAGS += -race 124 | CGO_ENABLED := 1 125 | GO_LDFLAGS += -linkmode=external -extldflags '-static' 126 | endif 127 | 128 | ifneq (, $(filter $(WITH_DEBUG), t true TRUE y yes 1)) 129 | GO_BUILDFLAGS += -tags sidero.debug 130 | else 131 | GO_LDFLAGS += -s 132 | endif 133 | 134 | all: unit-tests conform image-conform lint 135 | 136 | $(ARTIFACTS): ## Creates artifacts directory. 137 | @mkdir -p $(ARTIFACTS) 138 | 139 | .PHONY: clean 140 | clean: ## Cleans up all artifacts. 141 | @rm -rf $(ARTIFACTS) 142 | 143 | target-%: ## Builds the specified target defined in the Dockerfile. The build result will only remain in the build cache. 144 | @$(BUILD) --target=$* $(COMMON_ARGS) $(TARGET_ARGS) $(CI_ARGS) . 145 | 146 | local-%: ## Builds the specified target defined in the Dockerfile using the local output type. The build result will be output to the specified local destination. 147 | @$(MAKE) target-$* TARGET_ARGS="--output=type=local,dest=$(DEST) $(TARGET_ARGS)" 148 | 149 | generate: ## Generate .proto definitions. 150 | @$(MAKE) local-$@ DEST=./ 151 | 152 | lint-golangci-lint: ## Runs golangci-lint linter. 153 | @$(MAKE) target-$@ 154 | 155 | lint-gofumpt: ## Runs gofumpt linter. 156 | @$(MAKE) target-$@ 157 | 158 | .PHONY: fmt 159 | fmt: ## Formats the source code 160 | @docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \ 161 | bash -c "export GOTOOLCHAIN=local; \ 162 | export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \ 163 | go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \ 164 | gofumpt -w ." 165 | 166 | lint-govulncheck: ## Runs govulncheck linter. 167 | @$(MAKE) target-$@ 168 | 169 | .PHONY: base 170 | base: ## Prepare base toolchain 171 | @$(MAKE) target-$@ 172 | 173 | .PHONY: unit-tests 174 | unit-tests: ## Performs unit tests 175 | @$(MAKE) local-$@ DEST=$(ARTIFACTS) 176 | 177 | .PHONY: unit-tests-race 178 | unit-tests-race: ## Performs unit tests with race detection enabled. 179 | @$(MAKE) target-$@ 180 | 181 | .PHONY: $(ARTIFACTS)/conform-darwin-amd64 182 | $(ARTIFACTS)/conform-darwin-amd64: 183 | @$(MAKE) local-conform-darwin-amd64 DEST=$(ARTIFACTS) 184 | 185 | .PHONY: conform-darwin-amd64 186 | conform-darwin-amd64: $(ARTIFACTS)/conform-darwin-amd64 ## Builds executable for conform-darwin-amd64. 187 | 188 | .PHONY: $(ARTIFACTS)/conform-darwin-arm64 189 | $(ARTIFACTS)/conform-darwin-arm64: 190 | @$(MAKE) local-conform-darwin-arm64 DEST=$(ARTIFACTS) 191 | 192 | .PHONY: conform-darwin-arm64 193 | conform-darwin-arm64: $(ARTIFACTS)/conform-darwin-arm64 ## Builds executable for conform-darwin-arm64. 194 | 195 | .PHONY: $(ARTIFACTS)/conform-linux-amd64 196 | $(ARTIFACTS)/conform-linux-amd64: 197 | @$(MAKE) local-conform-linux-amd64 DEST=$(ARTIFACTS) 198 | 199 | .PHONY: conform-linux-amd64 200 | conform-linux-amd64: $(ARTIFACTS)/conform-linux-amd64 ## Builds executable for conform-linux-amd64. 201 | 202 | .PHONY: $(ARTIFACTS)/conform-linux-arm64 203 | $(ARTIFACTS)/conform-linux-arm64: 204 | @$(MAKE) local-conform-linux-arm64 DEST=$(ARTIFACTS) 205 | 206 | .PHONY: conform-linux-arm64 207 | conform-linux-arm64: $(ARTIFACTS)/conform-linux-arm64 ## Builds executable for conform-linux-arm64. 208 | 209 | .PHONY: conform 210 | conform: conform-darwin-amd64 conform-darwin-arm64 conform-linux-amd64 conform-linux-arm64 ## Builds executables for conform. 211 | 212 | .PHONY: lint-markdown 213 | lint-markdown: ## Runs markdownlint. 214 | @$(MAKE) target-$@ 215 | 216 | .PHONY: lint 217 | lint: lint-golangci-lint lint-gofumpt lint-govulncheck lint-markdown ## Run all linters for the project. 218 | 219 | .PHONY: image-conform 220 | image-conform: ## Builds image for conform. 221 | @$(MAKE) target-$@ TARGET_ARGS="--tag=$(REGISTRY)/$(USERNAME)/conform:$(IMAGE_TAG)" 222 | 223 | .PHONY: rekres 224 | rekres: 225 | @docker pull $(KRES_IMAGE) 226 | @docker run --rm --net=host --user $(shell id -u):$(shell id -g) -v $(PWD):/src -w /src -e GITHUB_TOKEN $(KRES_IMAGE) 227 | 228 | .PHONY: help 229 | help: ## This help menu. 230 | @echo "$$HELP_MENU_HEADER" 231 | @grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 232 | 233 | .PHONY: release-notes 234 | release-notes: $(ARTIFACTS) 235 | @ARTIFACTS=$(ARTIFACTS) ./hack/release.sh $@ $(ARTIFACTS)/RELEASE_NOTES.md $(TAG) 236 | 237 | .PHONY: conformance 238 | conformance: 239 | @docker pull $(CONFORMANCE_IMAGE) 240 | @docker run --rm -it -v $(PWD):/src -w /src $(CONFORMANCE_IMAGE) enforce 241 | 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |

Conform

5 |

Policy enforcement for your pipelines.

6 |

7 | Conventional Commits 8 | GoDoc 9 | Travis 10 | Codecov 11 | Go Report Card 12 | Release 13 | GitHub (pre-)release 14 |

15 |

16 | 17 | --- 18 | 19 | **Conform** is a tool for enforcing policies on your build pipelines. 20 | 21 | Some of the policies included are: 22 | 23 | - **Commits**: Enforce commit policies including: 24 | - Commit message header length 25 | - Developer Certificate of Origin 26 | - GPG signature 27 | - GPG signature identity check 28 | - [Conventional Commits](https://www.conventionalcommits.org) 29 | - Imperative mood 30 | - Spell check 31 | - Maximum of one commit ahead of `master` 32 | - Require a commit body 33 | - Jira issue check 34 | - **License Headers**: Enforce license headers on source code files. 35 | 36 | ## Getting Started 37 | 38 | To install conform you can download a [release](https://github.com/siderolabs/conform/releases), or build it locally (go must be installed): 39 | 40 | ```bash 41 | go install github.com/siderolabs/conform/cmd/conform@latest 42 | ``` 43 | 44 | Third option is to run it as a container: 45 | 46 | ```bash 47 | docker run --rm -it -v $PWD:/src:ro,Z -w /src ghcr.io/siderolabs/conform:v0.1.0-alpha.22 enforce 48 | ``` 49 | 50 | You can also install conform with [aqua](https://aquaproj.github.io/). 51 | 52 | ```bash 53 | aqua g -i siderolabs/conform 54 | ``` 55 | 56 | Now, create a file named `.conform.yaml` with the following contents: 57 | 58 | ```yaml 59 | policies: 60 | - type: commit 61 | spec: 62 | header: 63 | length: 89 64 | imperative: true 65 | case: lower 66 | invalidLastCharacters: . 67 | jira: 68 | keys: 69 | - PROJ 70 | - JIRA 71 | body: 72 | required: true 73 | dco: true 74 | gpg: 75 | required: false 76 | identity: 77 | gitHubOrganization: some-organization 78 | spellcheck: 79 | locale: US 80 | maximumOfOneCommit: true 81 | conventional: 82 | types: 83 | - "type" 84 | scopes: 85 | - "scope" 86 | descriptionLength: 72 87 | - type: license 88 | spec: 89 | skipPaths: 90 | - .git/ 91 | - .build*/ 92 | includeSuffixes: 93 | - .ext 94 | excludeSuffixes: 95 | - .exclude-ext-prefix.ext 96 | allowPrecedingComments: false 97 | header: | 98 | This is the contents of a license header. 99 | ``` 100 | 101 | In the same directory, run: 102 | 103 | ```bash 104 | $ conform enforce 105 | POLICY CHECK STATUS MESSAGE 106 | commit Header Length PASS Header is 43 characters 107 | commit Imperative Mood PASS Commit begins with imperative verb 108 | commit Header Case PASS Header case is valid 109 | commit Header Last Character PASS Header last character is valid 110 | commit DCO PASS Developer Certificate of Origin was found 111 | commit GPG PASS GPG signature found 112 | commit GPG Identity PASS Signed by "Someone " 113 | commit Conventional Commit PASS Commit message is a valid conventional commit 114 | commit Spellcheck PASS Commit contains 0 misspellings 115 | commit Number of Commits PASS HEAD is 0 commit(s) ahead of refs/heads/master 116 | commit Commit Body PASS Commit body is valid 117 | license File Header PASS All files have a valid license header 118 | ``` 119 | 120 | To setup a `commit-msg` hook: 121 | 122 | ```bash 123 | cat <..") 70 | enforceCmd.Flags().String("base-branch", "", "base branch to compare with") 71 | rootCmd.AddCommand(enforceCmd) 72 | } 73 | 74 | func detectMainBranch() (string, error) { 75 | mainBranch := "main" 76 | 77 | repo, err := git.PlainOpen(".") 78 | if err != nil { 79 | // not a git repo, ignore 80 | return "", nil //nolint:nilerr 81 | } 82 | 83 | c, err := repo.Config() 84 | if err != nil { 85 | return "", fmt.Errorf("failed to get repository configuration: %w", err) 86 | } 87 | 88 | rawConfig := c.Raw 89 | 90 | const branchSectionName = "branch" 91 | 92 | branchSection := rawConfig.Section(branchSectionName) 93 | for _, b := range branchSection.Subsections { 94 | remote := b.Option("remote") 95 | if remote == git.DefaultRemoteName { 96 | mainBranch = b.Name 97 | 98 | break 99 | } 100 | } 101 | 102 | return mainBranch, nil 103 | } 104 | -------------------------------------------------------------------------------- /cmd/conform/main.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package main 6 | 7 | func main() { 8 | Execute() 9 | } 10 | -------------------------------------------------------------------------------- /cmd/conform/root.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package main provides CLI commands. 6 | package main 7 | 8 | import ( 9 | "os" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // rootCmd represents the base command when called without any subcommands. 15 | var rootCmd = &cobra.Command{ 16 | Use: "conform", 17 | Short: "Policy enforcement for your pipelines.", 18 | Long: ``, 19 | } 20 | 21 | // Execute adds all child commands to the root command sets flags appropriately. 22 | // This is called by main.main(). It only needs to happen once to the rootCmd. 23 | func Execute() { 24 | if err := rootCmd.Execute(); err != nil { 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cmd/conform/serve.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/http" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | 17 | git "github.com/go-git/go-git/v5" 18 | "github.com/go-git/go-git/v5/config" 19 | "github.com/go-git/go-git/v5/plumbing" 20 | "github.com/google/go-github/v60/github" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | const ( 25 | path = "/github" 26 | ) 27 | 28 | // serveCmd represents the serve command. 29 | var serveCmd = &cobra.Command{ 30 | Use: "serve", 31 | Short: "", 32 | Long: ``, 33 | Run: func(_ *cobra.Command, _ []string) { 34 | if err := os.MkdirAll("/tmp", 0o700); err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { 39 | payload, err := io.ReadAll(r.Body) 40 | if err != nil { 41 | log.Printf("failed to read payload: %+v\n", err) 42 | 43 | return 44 | } 45 | 46 | go func() { 47 | dir, err := os.MkdirTemp("", "conform") 48 | if err != nil { 49 | log.Printf("failed to create temporary directory: %+v\n", err) 50 | 51 | return 52 | } 53 | 54 | defer os.RemoveAll(dir) //nolint:errcheck 55 | 56 | if err = os.MkdirAll(filepath.Join(dir, "github"), 0o700); err != nil { 57 | log.Printf("failed to create github directory: %+v\n", err) 58 | 59 | return 60 | } 61 | if err = os.MkdirAll(filepath.Join(dir, "repo"), 0o700); err != nil { 62 | log.Printf("failed to create repo directory: %+v\n", err) 63 | 64 | return 65 | } 66 | 67 | event := filepath.Join(dir, "github", "event.json") 68 | pullRequestEvent := &github.PullRequestEvent{} 69 | if err = json.Unmarshal(payload, pullRequestEvent); err != nil { 70 | log.Printf("failed to parse pull_request event: %+v\n", err) 71 | 72 | return 73 | } 74 | 75 | cloneRepo := filepath.Join(dir, "repo") 76 | cloneURL := pullRequestEvent.GetPullRequest().GetBase().GetRepo().GetCloneURL() 77 | 78 | log.Printf("Cloning %s", cloneURL) 79 | 80 | repo, err := git.PlainClone(cloneRepo, false, &git.CloneOptions{ 81 | SingleBranch: false, 82 | NoCheckout: true, 83 | URL: cloneURL, 84 | Progress: os.Stdout, 85 | }) 86 | if err != nil { 87 | log.Printf("failed to clone repo: %+v\n", err) 88 | 89 | return 90 | } 91 | 92 | id := pullRequestEvent.GetPullRequest().GetNumber() 93 | 94 | ref := pullRequestEvent.GetPullRequest().GetHead().GetRef() 95 | 96 | refSpec := fmt.Sprintf("refs/pull/%d/head:%s", id, ref) 97 | 98 | err = repo.Fetch(&git.FetchOptions{ 99 | RefSpecs: []config.RefSpec{ 100 | config.RefSpec("refs/heads/*:refs/heads/*"), 101 | config.RefSpec(refSpec), 102 | }, 103 | Progress: os.Stdout, 104 | }) 105 | if err != nil { 106 | log.Printf("failed to fetch %q: %v", refSpec, err) 107 | 108 | return 109 | } 110 | 111 | worktree, err := repo.Worktree() 112 | if err != nil { 113 | log.Printf("failed to get working tree: %v", err) 114 | 115 | return 116 | } 117 | 118 | err = worktree.Checkout(&git.CheckoutOptions{ 119 | Branch: plumbing.NewBranchReferenceName(ref), 120 | }) 121 | if err != nil { 122 | log.Printf("failed to checkout %q: %v", ref, err) 123 | 124 | return 125 | } 126 | 127 | log.Printf("writing %s to disk", event) 128 | 129 | if err = os.WriteFile(event, payload, 0o600); err != nil { 130 | log.Printf("failed to write event to disk: %+v\n", err) 131 | 132 | return 133 | } 134 | cmd := exec.Command("/proc/self/exe", "enforce", "--reporter=github", "--commit-ref=refs/heads/"+pullRequestEvent.GetPullRequest().GetBase().GetRef()) 135 | cmd.Stdout = os.Stdout 136 | cmd.Stderr = os.Stdout 137 | cmd.Dir = cloneRepo 138 | cmd.Env = []string{fmt.Sprintf("INPUT_TOKEN=%s", os.Getenv("INPUT_TOKEN")), fmt.Sprintf("GITHUB_EVENT_PATH=%s", event)} 139 | err = cmd.Start() 140 | if err != nil { 141 | log.Printf("failed to start command: %+v\n", err) 142 | 143 | return 144 | } 145 | err = cmd.Wait() 146 | if err != nil { 147 | log.Printf("command failed: %+v\n", err) 148 | 149 | return 150 | } 151 | }() 152 | 153 | w.WriteHeader(http.StatusOK) 154 | }) 155 | 156 | log.Fatal(http.ListenAndServe(":3000", nil)) 157 | }, 158 | } 159 | 160 | func init() { 161 | rootCmd.AddCommand(serveCmd) 162 | rootCmd.AddCommand(versionCmd) 163 | } 164 | -------------------------------------------------------------------------------- /cmd/conform/version.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/siderolabs/conform/internal/version" 13 | ) 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: "Prints Kres version.", 18 | Long: `Prints Kres version.`, 19 | Args: cobra.NoArgs, 20 | Run: func(*cobra.Command, []string) { 21 | line := fmt.Sprintf("%s version %s (%s)", version.Name, version.Tag, version.SHA) 22 | fmt.Println(line) 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/siderolabs/conform 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 7 | github.com/go-git/go-git/v5 v5.11.0 8 | github.com/golangci/misspell v0.4.1 9 | github.com/google/go-github/v60 v60.0.0 10 | github.com/jdkato/prose/v3 v3.0.0-20210921205322-a376476c2627 11 | github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 12 | github.com/mitchellh/mapstructure v1.5.0 13 | github.com/pkg/errors v0.9.1 14 | github.com/spf13/cobra v1.8.0 15 | github.com/stretchr/testify v1.8.4 16 | golang.org/x/sync v0.6.0 17 | gopkg.in/yaml.v3 v3.0.1 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.0 // indirect 22 | github.com/Microsoft/go-winio v0.6.1 // indirect 23 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 24 | github.com/cloudflare/circl v1.3.7 // indirect 25 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 26 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/emirpasic/gods v1.18.1 // indirect 29 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 30 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 31 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 32 | github.com/google/go-querystring v1.1.0 // indirect 33 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 34 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 35 | github.com/kevinburke/ssh_config v1.2.0 // indirect 36 | github.com/pjbgf/sha1cd v0.3.0 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/sergi/go-diff v1.3.1 // indirect 39 | github.com/skeema/knownhosts v1.2.1 // indirect 40 | github.com/spf13/pflag v1.0.5 // indirect 41 | github.com/xanzy/ssh-agent v0.3.3 // indirect 42 | golang.org/x/crypto v0.19.0 // indirect 43 | golang.org/x/mod v0.15.0 // indirect 44 | golang.org/x/net v0.21.0 // indirect 45 | golang.org/x/sys v0.17.0 // indirect 46 | golang.org/x/tools v0.18.0 // indirect 47 | gonum.org/v1/gonum v0.14.0 // indirect 48 | gopkg.in/neurosnap/sentences.v1 v1.0.7 // indirect 49 | gopkg.in/warnings.v0 v0.1.2 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 4 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 5 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 6 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 7 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 8 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= 9 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 10 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 11 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 13 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 14 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 15 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 16 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 18 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 19 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 20 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 21 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817 h1:0nsrg//Dc7xC74H/TZ5sYR8uk4UQRNjsw8zejqH5a4Q= 26 | github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817/go.mod h1:C/+sI4IFnEpCn6VQ3GIPEp+FrQnQw+YQP3+n+GdGq7o= 27 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 28 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 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/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 32 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= 33 | github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 35 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 36 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 37 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 39 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 40 | github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= 41 | github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= 42 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 44 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 45 | github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g= 46 | github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI= 47 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 48 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 49 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= 51 | github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk= 52 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 53 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 54 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 55 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 56 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 57 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 58 | github.com/jdkato/prose/v3 v3.0.0-20210921205322-a376476c2627 h1:3NE44NVT7k65KUAMN8ymYyl7iHU9sGI1f5Yoebd8Xng= 59 | github.com/jdkato/prose/v3 v3.0.0-20210921205322-a376476c2627/go.mod h1:Jhd9L9aYCc6gEqW9K3WZyybABly7+npj8nemQ3fuDx4= 60 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 61 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 62 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 63 | github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4 h1:cTxwSmnaqLoo+4tLukHoB9iqHOu3LmLhRmgUxZo6Vp4= 64 | github.com/keybase/go-crypto v0.0.0-20200123153347-de78d2cb44f4/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= 65 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 66 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 67 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 68 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 69 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 70 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 71 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 72 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 73 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 74 | github.com/neurosnap/sentences v1.0.6 h1:iBVUivNtlwGkYsJblWV8GGVFmXzZzak907Ci8aA0VTE= 75 | github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc= 76 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 77 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 78 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 79 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 80 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 81 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 82 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 83 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 85 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 86 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 87 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 88 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 89 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 90 | github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= 91 | github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 92 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 93 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 94 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 95 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 96 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 97 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 98 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 99 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 100 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 101 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 102 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 103 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 105 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 106 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 107 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 108 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 109 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 110 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 111 | golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 112 | golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 113 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 114 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= 115 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 116 | golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 117 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 118 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 119 | golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= 120 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 121 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 122 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 123 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 124 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 125 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 126 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 127 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 128 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 129 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 130 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 131 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 132 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 133 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 134 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 135 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 138 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 144 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 146 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 147 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 148 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 149 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 150 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 151 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 152 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 153 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 154 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 155 | golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= 156 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 157 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 158 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 159 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 160 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 161 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 162 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 163 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 164 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 165 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 166 | golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 167 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 168 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 169 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 170 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 171 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 172 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 173 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 174 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 175 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 176 | gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= 177 | gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= 178 | gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0= 179 | gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU= 180 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 181 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= 182 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 183 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 184 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 185 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 186 | gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= 187 | gopkg.in/neurosnap/sentences.v1 v1.0.7 h1:gpTUYnqthem4+o8kyTLiYIB05W+IvdQFYR29erfe8uU= 188 | gopkg.in/neurosnap/sentences.v1 v1.0.7/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0= 189 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 190 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 191 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 192 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 193 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 194 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 195 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 196 | -------------------------------------------------------------------------------- /hack/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 4 | # 5 | # Generated on 2024-10-10T13:02:45Z by kres 34e72ac. 6 | 7 | set -e 8 | 9 | RELEASE_TOOL_IMAGE="ghcr.io/siderolabs/release-tool:latest" 10 | 11 | function release-tool { 12 | docker pull "${RELEASE_TOOL_IMAGE}" >/dev/null 13 | docker run --rm -w /src -v "${PWD}":/src:ro "${RELEASE_TOOL_IMAGE}" -l -d -n -t "${1}" ./hack/release.toml 14 | } 15 | 16 | function changelog { 17 | if [ "$#" -eq 1 ]; then 18 | (release-tool ${1}; echo; cat CHANGELOG.md) > CHANGELOG.md- && mv CHANGELOG.md- CHANGELOG.md 19 | else 20 | echo 1>&2 "Usage: $0 changelog [tag]" 21 | exit 1 22 | fi 23 | } 24 | 25 | function release-notes { 26 | release-tool "${2}" > "${1}" 27 | } 28 | 29 | function cherry-pick { 30 | if [ $# -ne 2 ]; then 31 | echo 1>&2 "Usage: $0 cherry-pick " 32 | exit 1 33 | fi 34 | 35 | git checkout $2 36 | git fetch 37 | git rebase upstream/$2 38 | git cherry-pick -x $1 39 | } 40 | 41 | function commit { 42 | if [ $# -ne 1 ]; then 43 | echo 1>&2 "Usage: $0 commit " 44 | exit 1 45 | fi 46 | 47 | if is_on_main_branch; then 48 | update_license_files 49 | fi 50 | 51 | git commit -s -m "release($1): prepare release" -m "This is the official $1 release." 52 | } 53 | 54 | function is_on_main_branch { 55 | main_remotes=("upstream" "origin") 56 | branch_names=("main" "master") 57 | current_branch=$(git rev-parse --abbrev-ref HEAD) 58 | 59 | echo "Check current branch: $current_branch" 60 | 61 | for remote in "${main_remotes[@]}"; do 62 | echo "Fetch remote $remote..." 63 | 64 | if ! git fetch --quiet "$remote" &>/dev/null; then 65 | echo "Failed to fetch $remote, skip..." 66 | 67 | continue 68 | fi 69 | 70 | for branch_name in "${branch_names[@]}"; do 71 | if ! git rev-parse --verify "$branch_name" &>/dev/null; then 72 | echo "Branch $branch_name does not exist, skip..." 73 | 74 | continue 75 | fi 76 | 77 | echo "Branch $remote/$branch_name exists, comparing..." 78 | 79 | merge_base=$(git merge-base "$current_branch" "$remote/$branch_name") 80 | latest_main=$(git rev-parse "$remote/$branch_name") 81 | 82 | if [ "$merge_base" = "$latest_main" ]; then 83 | echo "Current branch is up-to-date with $remote/$branch_name" 84 | 85 | return 0 86 | else 87 | echo "Current branch is not on $remote/$branch_name" 88 | 89 | return 1 90 | fi 91 | done 92 | done 93 | 94 | echo "No main or master branch found on any remote" 95 | 96 | return 1 97 | } 98 | 99 | function update_license_files { 100 | script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 101 | parent_dir="$(dirname "$script_dir")" 102 | current_year=$(date +"%Y") 103 | change_date=$(date -v+4y +"%Y-%m-%d" 2>/dev/null || date -d "+4 years" +"%Y-%m-%d" 2>/dev/null || date --date="+4 years" +"%Y-%m-%d") 104 | 105 | # Find LICENSE and .kres.yaml files recursively in the parent directory (project root) 106 | find "$parent_dir" \( -name "LICENSE" -o -name ".kres.yaml" \) -type f | while read -r file; do 107 | temp_file="${file}.tmp" 108 | 109 | if [[ $file == *"LICENSE" ]]; then 110 | if grep -q "^Business Source License" "$file"; then 111 | sed -e "s/The Licensed Work is (c) [0-9]\{4\}/The Licensed Work is (c) $current_year/" \ 112 | -e "s/Change Date: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/Change Date: $change_date/" \ 113 | "$file" >"$temp_file" 114 | else 115 | continue # Not a Business Source License file 116 | fi 117 | elif [[ $file == *".kres.yaml" ]]; then 118 | sed -E 's/^([[:space:]]*)ChangeDate:.*$/\1ChangeDate: "'"$change_date"'"/' "$file" >"$temp_file" 119 | fi 120 | 121 | # Check if the file has changed 122 | if ! cmp -s "$file" "$temp_file"; then 123 | mv "$temp_file" "$file" 124 | echo "Updated: $file" 125 | git add "$file" 126 | else 127 | echo "No changes: $file" 128 | rm "$temp_file" 129 | fi 130 | done 131 | } 132 | 133 | if declare -f "$1" > /dev/null 134 | then 135 | cmd="$1" 136 | shift 137 | $cmd "$@" 138 | else 139 | cat < 1 { 68 | parents := commit.Parents() 69 | 70 | for i := 1; i <= commit.NumParents(); i++ { 71 | var next *object.Commit 72 | 73 | next, err = parents.Next() 74 | if err != nil { 75 | return "", err 76 | } 77 | 78 | if i == commit.NumParents() { 79 | message = next.Message 80 | } 81 | } 82 | } else { 83 | message = commit.Message 84 | } 85 | 86 | return message, err 87 | } 88 | 89 | // Messages returns the list of commit messages in the range commit1..commit2. 90 | func (g *Git) Messages(commit1, commit2 string) ([]string, error) { 91 | hash1, err := g.repo.ResolveRevision(plumbing.Revision(commit1)) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | hash2, err := g.repo.ResolveRevision(plumbing.Revision(commit2)) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | c2, err := g.repo.CommitObject(*hash2) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | c1, err := g.repo.CommitObject(*hash1) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if ok, ancestorErr := c1.IsAncestor(c2); ancestorErr != nil || !ok { 112 | c, mergeBaseErr := c1.MergeBase(c2) 113 | if mergeBaseErr != nil { 114 | return nil, errors.Errorf("invalid ancestor %s", c1) 115 | } 116 | 117 | c1 = c[0] 118 | } 119 | 120 | msgs := make([]string, 0) 121 | 122 | for { 123 | msgs = append(msgs, c2.Message) 124 | 125 | c2, err = c2.Parents().Next() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | if c2.ID() == c1.ID() { 131 | break 132 | } 133 | } 134 | 135 | return msgs, nil 136 | } 137 | 138 | // HasGPGSignature returns the commit message. In the case that a commit has multiple 139 | // parents, the message of the last parent is returned. 140 | // 141 | //nolint:nonamedreturns 142 | func (g *Git) HasGPGSignature() (ok bool, err error) { 143 | ref, err := g.repo.Head() 144 | if err != nil { 145 | return false, err 146 | } 147 | 148 | commit, err := g.repo.CommitObject(ref.Hash()) 149 | if err != nil { 150 | return false, err 151 | } 152 | 153 | ok = commit.PGPSignature != "" 154 | 155 | return ok, err 156 | } 157 | 158 | // VerifyPGPSignature validates PGP signature against a keyring. 159 | func (g *Git) VerifyPGPSignature(armoredKeyrings []string) (*openpgp.Entity, error) { 160 | ref, err := g.repo.Head() 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | commit, err := g.repo.CommitObject(ref.Hash()) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | var keyring openpgp.EntityList 171 | 172 | for _, armoredKeyring := range armoredKeyrings { 173 | var el openpgp.EntityList 174 | 175 | el, err = openpgp.ReadArmoredKeyRing(strings.NewReader(armoredKeyring)) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | keyring = append(keyring, el...) 181 | } 182 | 183 | // Extract signature. 184 | signature := strings.NewReader(commit.PGPSignature) 185 | 186 | encoded := &plumbing.MemoryObject{} 187 | 188 | // Encode commit components, excluding signature and get a reader object. 189 | if err = commit.EncodeWithoutSignature(encoded); err != nil { 190 | return nil, err 191 | } 192 | 193 | er, err := encoded.Reader() 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | return openpgp.CheckArmoredDetachedSignature(keyring, er, signature) 199 | } 200 | 201 | // FetchPullRequest fetches a remote PR. 202 | // 203 | //nolint:nonamedreturns 204 | func (g *Git) FetchPullRequest(remote string, number int) (err error) { 205 | opts := &git.FetchOptions{ 206 | RemoteName: remote, 207 | RefSpecs: []config.RefSpec{ 208 | config.RefSpec(fmt.Sprintf("refs/pull/%d/head:pr/%d", number, number)), 209 | }, 210 | } 211 | 212 | return g.repo.Fetch(opts) 213 | } 214 | 215 | // CheckoutPullRequest checks out pull request. 216 | // 217 | //nolint:nonamedreturns 218 | func (g *Git) CheckoutPullRequest(number int) (err error) { 219 | w, err := g.repo.Worktree() 220 | if err != nil { 221 | return err 222 | } 223 | 224 | opts := &git.CheckoutOptions{ 225 | Branch: plumbing.ReferenceName(fmt.Sprintf("pr/%d", number)), 226 | } 227 | 228 | return w.Checkout(opts) 229 | } 230 | 231 | // SHA returns the sha of the current commit. 232 | // 233 | //nolint:nonamedreturns 234 | func (g *Git) SHA() (sha string, err error) { 235 | ref, err := g.repo.Head() 236 | if err != nil { 237 | return sha, err 238 | } 239 | 240 | sha = ref.Hash().String() 241 | 242 | return sha, nil 243 | } 244 | 245 | // AheadBehind returns the number of commits that HEAD is ahead and behind 246 | // relative to the specified ref. 247 | // 248 | //nolint:nonamedreturns 249 | func (g *Git) AheadBehind(ref string) (ahead, behind int, err error) { 250 | ref1, err := g.repo.Reference(plumbing.ReferenceName(ref), false) 251 | if err != nil { 252 | return 0, 0, err 253 | } 254 | 255 | ref2, err := g.repo.Head() 256 | if err != nil { 257 | return 0, 0, err 258 | } 259 | 260 | commit2, err := object.GetCommit(g.repo.Storer, ref2.Hash()) 261 | if err != nil { 262 | return 0, 0, nil //nolint:nilerr 263 | } 264 | 265 | var count int 266 | 267 | iter := object.NewCommitPreorderIter(commit2, nil, nil) 268 | 269 | err = iter.ForEach(func(comm *object.Commit) error { 270 | if comm.Hash != ref1.Hash() { 271 | count++ 272 | 273 | return nil 274 | } 275 | 276 | return storer.ErrStop 277 | }) 278 | if err != nil { 279 | return 0, 0, nil //nolint:nilerr 280 | } 281 | 282 | return count, 0, nil 283 | } 284 | -------------------------------------------------------------------------------- /internal/policy/commit/check_body.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/siderolabs/conform/internal/policy" 13 | ) 14 | 15 | // RequiredBodyThreshold is the default minimum number of line changes required 16 | // to trigger the body check. 17 | var RequiredBodyThreshold = 10 18 | 19 | // Body enforces a maximum number of charcters on the commit 20 | // header. 21 | type Body struct { 22 | errors []error 23 | } 24 | 25 | // Name returns the name of the check. 26 | func (h Body) Name() string { 27 | return "Commit Body" 28 | } 29 | 30 | // Message returns to check message. 31 | func (h Body) Message() string { 32 | if len(h.errors) != 0 { 33 | return h.errors[0].Error() 34 | } 35 | 36 | return "Commit body is valid" 37 | } 38 | 39 | // Errors returns any violations of the check. 40 | func (h Body) Errors() []error { 41 | return h.errors 42 | } 43 | 44 | // ValidateBody checks the header length. 45 | func (c Commit) ValidateBody() policy.Check { //nolint:ireturn 46 | check := &Body{} 47 | 48 | lines := strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n") 49 | valid := false 50 | 51 | for _, line := range lines[1:] { 52 | if DCORegex.MatchString(strings.TrimSpace(line)) { 53 | continue 54 | } 55 | 56 | if line != "" { 57 | valid = true 58 | 59 | break 60 | } 61 | } 62 | 63 | if !valid { 64 | check.errors = append(check.errors, errors.New("Commit body is empty")) 65 | } 66 | 67 | return check 68 | } 69 | -------------------------------------------------------------------------------- /internal/policy/commit/check_conventional_commit.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // Conventional implements the policy.Policy interface and enforces commit 17 | // messages to conform the Conventional Commit standard. 18 | type Conventional struct { 19 | Types []string `mapstructure:"types"` 20 | Scopes []string `mapstructure:"scopes"` 21 | DescriptionLength int `mapstructure:"descriptionLength"` 22 | } 23 | 24 | // HeaderRegex is the regular expression used for Conventional Commits 1.0.0. 25 | var HeaderRegex = regexp.MustCompile(`^(\w*)(\(([^)]+)\))?(!)?:\s{1}(.*)($|\n{2})`) 26 | 27 | const ( 28 | // TypeFeat is a commit of the type fix patches a bug in your codebase 29 | // (this correlates with MINOR in semantic versioning). 30 | TypeFeat = "feat" 31 | 32 | // TypeFix is a commit of the type feat introduces a new feature to the 33 | // codebase (this correlates with PATCH in semantic versioning). 34 | TypeFix = "fix" 35 | ) 36 | 37 | // ConventionalCommitCheck ensures that the commit message is a valid 38 | // conventional commit. 39 | type ConventionalCommitCheck struct { 40 | errors []error 41 | } 42 | 43 | // Name returns the name of the check. 44 | func (c ConventionalCommitCheck) Name() string { 45 | return "Conventional Commit" 46 | } 47 | 48 | // Message returns to check message. 49 | func (c ConventionalCommitCheck) Message() string { 50 | if len(c.errors) != 0 { 51 | return c.errors[0].Error() 52 | } 53 | 54 | return "Commit message is a valid conventional commit" 55 | } 56 | 57 | // Errors returns any violations of the check. 58 | func (c ConventionalCommitCheck) Errors() []error { 59 | return c.errors 60 | } 61 | 62 | // ValidateConventionalCommit returns the commit type. 63 | func (c Commit) ValidateConventionalCommit() policy.Check { //nolint:ireturn 64 | check := &ConventionalCommitCheck{} 65 | groups := parseHeader(c.msg) 66 | 67 | if len(groups) != 7 { 68 | check.errors = append(check.errors, errors.Errorf("Invalid conventional commits format: %q", c.msg)) 69 | 70 | return check 71 | } 72 | 73 | // conventional commit sections 74 | ccType := groups[1] 75 | ccScope := groups[3] 76 | ccDesc := groups[5] 77 | 78 | c.Conventional.Types = append(c.Conventional.Types, TypeFeat, TypeFix) 79 | typeIsValid := false 80 | 81 | for _, t := range c.Conventional.Types { 82 | if t == ccType { 83 | typeIsValid = true 84 | } 85 | } 86 | 87 | if !typeIsValid { 88 | check.errors = append(check.errors, errors.Errorf("Invalid type %q: allowed types are %v", groups[1], c.Conventional.Types)) 89 | 90 | return check 91 | } 92 | 93 | // Scope is optional. 94 | if ccScope != "" { 95 | scopeIsValid := false 96 | 97 | for _, scope := range c.Conventional.Scopes { 98 | re := regexp.MustCompile(scope) 99 | if re.MatchString(ccScope) { 100 | scopeIsValid = true 101 | 102 | break 103 | } 104 | } 105 | 106 | if !scopeIsValid { 107 | check.errors = append(check.errors, errors.Errorf("Invalid scope %q: allowed scopes are %v", groups[3], c.Conventional.Scopes)) 108 | 109 | return check 110 | } 111 | } 112 | 113 | // Provide a good default value for DescriptionLength 114 | if c.Conventional.DescriptionLength == 0 { 115 | c.Conventional.DescriptionLength = 72 116 | } 117 | 118 | if len(ccDesc) <= c.Conventional.DescriptionLength && len(ccDesc) != 0 { 119 | return check 120 | } 121 | 122 | check.errors = append(check.errors, errors.Errorf("Invalid description: %s", ccDesc)) 123 | 124 | return check 125 | } 126 | 127 | func parseHeader(msg string) []string { 128 | // To circumvent any policy violation due to the leading \n that GitHub 129 | // prefixes to the commit message on a squash merge, we remove it from the 130 | // message. 131 | header := strings.Split(strings.TrimPrefix(msg, "\n"), "\n")[0] 132 | groups := HeaderRegex.FindStringSubmatch(header) 133 | 134 | return groups 135 | } 136 | -------------------------------------------------------------------------------- /internal/policy/commit/check_dco.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // DCORegex is the regular expression used for Developer Certificate of Origin. 17 | var DCORegex = regexp.MustCompile(`^Signed-off-by: ([^<]+) <([^<>@]+@[^<>]+)>$`) 18 | 19 | // DCOCheck ensures that the commit message contains a 20 | // Developer Certificate of Origin. 21 | type DCOCheck struct { 22 | errors []error 23 | } 24 | 25 | // Name returns the name of the check. 26 | func (d DCOCheck) Name() string { 27 | return "DCO" 28 | } 29 | 30 | // Message returns to check message. 31 | func (d DCOCheck) Message() string { 32 | if len(d.errors) != 0 { 33 | return d.errors[0].Error() 34 | } 35 | 36 | return "Developer Certificate of Origin was found" 37 | } 38 | 39 | // Errors returns any violations of the check. 40 | func (d DCOCheck) Errors() []error { 41 | return d.errors 42 | } 43 | 44 | // ValidateDCO checks the commit message for a Developer Certificate of Origin. 45 | func (c Commit) ValidateDCO() policy.Check { //nolint:ireturn 46 | check := &DCOCheck{} 47 | 48 | for _, line := range strings.Split(c.msg, "\n") { 49 | if DCORegex.MatchString(strings.TrimSpace(line)) { 50 | return check 51 | } 52 | } 53 | 54 | check.errors = append(check.errors, errors.Errorf("Commit does not have a DCO")) 55 | 56 | return check 57 | } 58 | -------------------------------------------------------------------------------- /internal/policy/commit/check_gpg_identity.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "sync" 13 | 14 | "github.com/google/go-github/v60/github" 15 | "golang.org/x/sync/errgroup" 16 | 17 | "github.com/siderolabs/conform/internal/git" 18 | "github.com/siderolabs/conform/internal/policy" 19 | ) 20 | 21 | // GPGIdentityCheck ensures that the commit is cryptographically signed using known identity. 22 | // 23 | //nolint:govet 24 | type GPGIdentityCheck struct { 25 | errors []error 26 | identity string 27 | } 28 | 29 | // Name returns the name of the check. 30 | func (g GPGIdentityCheck) Name() string { 31 | return "GPG Identity" 32 | } 33 | 34 | // Message returns to check message. 35 | func (g GPGIdentityCheck) Message() string { 36 | if len(g.errors) != 0 { 37 | return g.errors[0].Error() 38 | } 39 | 40 | return fmt.Sprintf("Signed by %q", g.identity) 41 | } 42 | 43 | // Errors returns any violations of the check. 44 | func (g GPGIdentityCheck) Errors() []error { 45 | return g.errors 46 | } 47 | 48 | // ValidateGPGIdentity checks the commit GPG signature for a known identity. 49 | func (c Commit) ValidateGPGIdentity(g *git.Git) policy.Check { //nolint:ireturn 50 | check := &GPGIdentityCheck{} 51 | 52 | switch { 53 | case c.GPG.Identity.GitHubOrganization != "": 54 | githubClient := github.NewClient(nil) 55 | 56 | list, _, err := githubClient.Organizations.ListMembers(context.Background(), c.GPG.Identity.GitHubOrganization, &github.ListMembersOptions{}) 57 | if err != nil { 58 | check.errors = append(check.errors, err) 59 | 60 | return check 61 | } 62 | 63 | members := make([]string, len(list)) 64 | 65 | for i := range list { 66 | members[i] = list[i].GetLogin() 67 | } 68 | 69 | keyrings, err := getKeyring(context.Background(), members) 70 | if err != nil { 71 | check.errors = append(check.errors, err) 72 | 73 | return check 74 | } 75 | 76 | entity, err := g.VerifyPGPSignature(keyrings) 77 | if err != nil { 78 | check.errors = append(check.errors, err) 79 | 80 | return check 81 | } 82 | 83 | for identity := range entity.Identities { 84 | check.identity = identity 85 | 86 | break 87 | } 88 | default: 89 | check.errors = append(check.errors, fmt.Errorf("no signature identity configuration found")) 90 | } 91 | 92 | return check 93 | } 94 | 95 | func getKeyring(ctx context.Context, members []string) ([]string, error) { 96 | var ( 97 | result []string 98 | mu sync.Mutex 99 | ) 100 | 101 | eg, ctx := errgroup.WithContext(ctx) 102 | 103 | for _, member := range members { 104 | eg.Go(func() error { 105 | key, err := getKey(ctx, member) 106 | 107 | mu.Lock() 108 | result = append(result, key) 109 | mu.Unlock() 110 | 111 | return err 112 | }) 113 | } 114 | 115 | err := eg.Wait() 116 | 117 | return result, err 118 | } 119 | 120 | func getKey(ctx context.Context, login string) (string, error) { 121 | // GitHub client doesn't have a method to fetch a key unauthenticated 122 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://github.com/%s.gpg", login), nil) 123 | if err != nil { 124 | return "", err 125 | } 126 | 127 | resp, err := http.DefaultClient.Do(req) 128 | if err != nil { 129 | return "", err 130 | } 131 | 132 | defer resp.Body.Close() //nolint:errcheck 133 | 134 | buf, err := io.ReadAll(resp.Body) 135 | 136 | return string(buf), err 137 | } 138 | -------------------------------------------------------------------------------- /internal/policy/commit/check_gpg_signature.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "github.com/pkg/errors" 9 | 10 | "github.com/siderolabs/conform/internal/git" 11 | "github.com/siderolabs/conform/internal/policy" 12 | ) 13 | 14 | // GPGCheck ensures that the commit is cryptographically signed using GPG. 15 | type GPGCheck struct { 16 | errors []error 17 | } 18 | 19 | // Name returns the name of the check. 20 | func (g GPGCheck) Name() string { 21 | return "GPG" 22 | } 23 | 24 | // Message returns to check message. 25 | func (g GPGCheck) Message() string { 26 | if len(g.errors) != 0 { 27 | return g.errors[0].Error() 28 | } 29 | 30 | return "GPG signature found" 31 | } 32 | 33 | // Errors returns any violations of the check. 34 | func (g GPGCheck) Errors() []error { 35 | return g.errors 36 | } 37 | 38 | // ValidateGPGSign checks the commit message for a GPG signature. 39 | func (c Commit) ValidateGPGSign(g *git.Git) policy.Check { //nolint:ireturn 40 | check := &GPGCheck{} 41 | 42 | ok, err := g.HasGPGSignature() 43 | if err != nil { 44 | check.errors = append(check.errors, err) 45 | 46 | return check 47 | } 48 | 49 | if !ok { 50 | check.errors = append(check.errors, errors.Errorf("Commit does not have a GPG signature")) 51 | 52 | return check 53 | } 54 | 55 | return check 56 | } 57 | -------------------------------------------------------------------------------- /internal/policy/commit/check_header_case.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "unicode" 9 | "unicode/utf8" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // HeaderCaseCheck enforces the case of the first word in the header. 17 | type HeaderCaseCheck struct { 18 | headerCase string 19 | errors []error 20 | } 21 | 22 | // Name returns the name of the check. 23 | func (h HeaderCaseCheck) Name() string { 24 | return "Header Case" 25 | } 26 | 27 | // Message returns to check message. 28 | func (h HeaderCaseCheck) Message() string { 29 | if len(h.errors) != 0 { 30 | return h.errors[0].Error() 31 | } 32 | 33 | return "Header case is valid" 34 | } 35 | 36 | // Errors returns any violations of the check. 37 | func (h HeaderCaseCheck) Errors() []error { 38 | return h.errors 39 | } 40 | 41 | // ValidateHeaderCase checks the header length. 42 | func (c Commit) ValidateHeaderCase() policy.Check { //nolint:ireturn 43 | check := &HeaderCaseCheck{headerCase: c.Header.Case} 44 | 45 | firstWord, err := c.firstWord() 46 | if err != nil { 47 | check.errors = append(check.errors, err) 48 | 49 | return check 50 | } 51 | 52 | first, _ := utf8.DecodeRuneInString(firstWord) 53 | if first == utf8.RuneError { 54 | check.errors = append(check.errors, errors.New("Header does not start with valid UTF-8 text")) 55 | 56 | return check 57 | } 58 | 59 | var valid bool 60 | 61 | switch c.Header.Case { 62 | case "upper": 63 | valid = unicode.IsUpper(first) 64 | case "lower": 65 | valid = unicode.IsLower(first) 66 | default: 67 | check.errors = append(check.errors, errors.Errorf("Invalid configured case %s", c.Header.Case)) 68 | 69 | return check 70 | } 71 | 72 | if !valid { 73 | check.errors = append(check.errors, errors.Errorf("Commit header case is not %s", c.Header.Case)) 74 | } 75 | 76 | return check 77 | } 78 | -------------------------------------------------------------------------------- /internal/policy/commit/check_header_last_character.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "strings" 9 | "unicode/utf8" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // HeaderLastCharacterCheck enforces that the last character of the header isn't in some set. 17 | type HeaderLastCharacterCheck struct { 18 | errors []error 19 | } 20 | 21 | // Name returns the name of the check. 22 | func (h HeaderLastCharacterCheck) Name() string { 23 | return "Header Last Character" 24 | } 25 | 26 | // Message returns to check message. 27 | func (h HeaderLastCharacterCheck) Message() string { 28 | if len(h.errors) != 0 { 29 | return h.errors[0].Error() 30 | } 31 | 32 | return "Header last character is valid" 33 | } 34 | 35 | // Errors returns any violations of the check. 36 | func (h HeaderLastCharacterCheck) Errors() []error { 37 | return h.errors 38 | } 39 | 40 | // ValidateHeaderLastCharacter checks the last character of the header. 41 | func (c Commit) ValidateHeaderLastCharacter() policy.Check { //nolint:ireturn 42 | check := &HeaderLastCharacterCheck{} 43 | 44 | switch last, _ := utf8.DecodeLastRuneInString(c.header()); { 45 | case last == utf8.RuneError: 46 | check.errors = append(check.errors, errors.New("Header does not end with valid UTF-8 text")) 47 | case strings.ContainsRune(c.Header.InvalidLastCharacters, last): 48 | check.errors = append(check.errors, errors.Errorf("Commit header ends in %q", last)) 49 | } 50 | 51 | return check 52 | } 53 | -------------------------------------------------------------------------------- /internal/policy/commit/check_header_length.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/siderolabs/conform/internal/policy" 13 | ) 14 | 15 | // MaxNumberOfCommitCharacters is the default maximium number of characters 16 | // allowed in a commit header. 17 | var MaxNumberOfCommitCharacters = 89 18 | 19 | // HeaderLengthCheck enforces a maximum number of charcters on the commit 20 | // header. 21 | // 22 | //nolint:govet 23 | type HeaderLengthCheck struct { 24 | headerLength int 25 | errors []error 26 | } 27 | 28 | // Name returns the name of the check. 29 | func (h HeaderLengthCheck) Name() string { 30 | return "Header Length" 31 | } 32 | 33 | // Message returns to check message. 34 | func (h HeaderLengthCheck) Message() string { 35 | return fmt.Sprintf("Header is %d characters", h.headerLength) 36 | } 37 | 38 | // Errors returns any violations of the check. 39 | func (h HeaderLengthCheck) Errors() []error { 40 | return h.errors 41 | } 42 | 43 | // ValidateHeaderLength checks the header length. 44 | func (c Commit) ValidateHeaderLength() policy.Check { //nolint:ireturn 45 | check := &HeaderLengthCheck{} 46 | 47 | if c.Header.Length != 0 { 48 | MaxNumberOfCommitCharacters = c.Header.Length 49 | } 50 | 51 | check.headerLength = len(c.header()) 52 | if check.headerLength > MaxNumberOfCommitCharacters { 53 | check.errors = append(check.errors, errors.Errorf("Commit header is %d characters", check.headerLength)) 54 | } 55 | 56 | return check 57 | } 58 | -------------------------------------------------------------------------------- /internal/policy/commit/check_imperative_verb.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/jdkato/prose/v3" 11 | "github.com/pkg/errors" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // ImperativeCheck enforces that the first word of a commit message header is 17 | // and imperative verb. 18 | type ImperativeCheck struct { 19 | errors []error 20 | } 21 | 22 | // Name returns the name of the check. 23 | func (i ImperativeCheck) Name() string { 24 | return "Imperative Mood" 25 | } 26 | 27 | // Message returns to check message. 28 | func (i ImperativeCheck) Message() string { 29 | if len(i.errors) != 0 { 30 | return i.errors[0].Error() 31 | } 32 | 33 | return "Commit begins with imperative verb" 34 | } 35 | 36 | // Errors returns any violations of the check. 37 | func (i ImperativeCheck) Errors() []error { 38 | return i.errors 39 | } 40 | 41 | // ValidateImperative checks the commit message for a GPG signature. 42 | func (c Commit) ValidateImperative() policy.Check { //nolint:ireturn 43 | check := &ImperativeCheck{} 44 | 45 | var ( 46 | word string 47 | err error 48 | ) 49 | 50 | if word, err = c.firstWord(); err != nil { 51 | check.errors = append(check.errors, err) 52 | 53 | return check 54 | } 55 | 56 | doc, err := prose.NewDocument("I " + strings.ToLower(word)) 57 | if err != nil { 58 | check.errors = append(check.errors, errors.Errorf("Failed to create document: %v", err)) 59 | 60 | return check 61 | } 62 | 63 | if len(doc.Tokens()) != 2 { 64 | check.errors = append(check.errors, errors.Errorf("Expected 2 tokens, got %d", len(doc.Tokens()))) 65 | 66 | return check 67 | } 68 | 69 | tokens := doc.Tokens() 70 | tok := tokens[1] 71 | 72 | for _, tag := range []string{"VBD", "VBG", "VBZ"} { 73 | if tok.Tag == tag { 74 | check.errors = append(check.errors, errors.Errorf("First word of commit must be an imperative verb: %q is invalid", word)) 75 | } 76 | } 77 | 78 | return check 79 | } 80 | -------------------------------------------------------------------------------- /internal/policy/commit/check_jira.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "regexp" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/siderolabs/conform/internal/policy" 13 | ) 14 | 15 | // JiraCheck enforces that a Jira issue is mentioned in the header. 16 | type JiraCheck struct { 17 | errors []error 18 | } 19 | 20 | // Name returns the name of the check. 21 | func (j *JiraCheck) Name() string { 22 | return "Jira issues" 23 | } 24 | 25 | // Message returns to check message. 26 | func (j *JiraCheck) Message() string { 27 | if len(j.errors) != 0 { 28 | return j.errors[0].Error() 29 | } 30 | 31 | return "Jira issues are valid" 32 | } 33 | 34 | // Errors returns any violations of the check. 35 | func (j *JiraCheck) Errors() []error { 36 | return j.errors 37 | } 38 | 39 | // ValidateJiraCheck validates if a Jira issue is mentioned in the header. 40 | func (c Commit) ValidateJiraCheck() policy.Check { //nolint:ireturn 41 | check := &JiraCheck{} 42 | 43 | reg := regexp.MustCompile(`.* \[?([A-Z]*)-[1-9]{1}\d*\]?.*`) 44 | 45 | if reg.MatchString(c.msg) { 46 | submatch := reg.FindStringSubmatch(c.msg) 47 | jiraProject := submatch[1] 48 | 49 | if !find(c.Header.Jira.Keys, jiraProject) { 50 | check.errors = append(check.errors, errors.Errorf("Jira project %s is not a valid jira project", jiraProject)) 51 | } 52 | } else { 53 | check.errors = append(check.errors, errors.Errorf("No Jira issue tag found in %q", c.msg)) 54 | } 55 | 56 | return check 57 | } 58 | 59 | func find(slice []string, value string) bool { 60 | for _, elem := range slice { 61 | if elem == value { 62 | return true 63 | } 64 | } 65 | 66 | return false 67 | } 68 | -------------------------------------------------------------------------------- /internal/policy/commit/check_jira_test.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | //nolint:testpackage 6 | package commit 7 | 8 | import ( 9 | "testing" 10 | ) 11 | 12 | func TestCommit_ValidateJiraCheck(t *testing.T) { 13 | //nolint:govet 14 | type fields struct { 15 | SpellCheck *SpellCheck 16 | Conventional *Conventional 17 | Header *HeaderChecks 18 | Body *BodyChecks 19 | DCO bool 20 | GPG bool 21 | MaximumOfOneCommit bool 22 | msg string 23 | } 24 | 25 | type want struct { 26 | errorCount int 27 | } 28 | 29 | tests := []struct { 30 | name string 31 | fields fields 32 | want want 33 | }{ 34 | { 35 | name: "Missing jira issue no type", 36 | fields: fields{ 37 | Header: &HeaderChecks{ 38 | Jira: &JiraChecks{ 39 | Keys: []string{"JIRA", "PROJ"}, 40 | }, 41 | }, 42 | msg: "invalid commit", 43 | }, 44 | want: want{errorCount: 1}, 45 | }, 46 | { 47 | name: "Missing jira issue with type", 48 | fields: fields{ 49 | Header: &HeaderChecks{ 50 | Jira: &JiraChecks{ 51 | Keys: []string{"JIRA", "PROJ"}, 52 | }, 53 | }, 54 | msg: "fix: invalid commit", 55 | }, 56 | want: want{errorCount: 1}, 57 | }, 58 | { 59 | name: "Valid commit", 60 | fields: fields{ 61 | Header: &HeaderChecks{ 62 | Jira: &JiraChecks{ 63 | Keys: []string{"JIRA", "PROJ"}, 64 | }, 65 | }, 66 | msg: "fix: [JIRA-1234] valid commit", 67 | }, 68 | want: want{errorCount: 0}, 69 | }, 70 | { 71 | name: "Valid commit 2", 72 | fields: fields{ 73 | Header: &HeaderChecks{ 74 | Jira: &JiraChecks{ 75 | Keys: []string{"JIRA", "PROJ"}, 76 | }, 77 | }, 78 | msg: "fix: [PROJ-1234] valid commit", 79 | }, 80 | want: want{errorCount: 0}, 81 | }, 82 | { 83 | name: "Invalid jira project", 84 | fields: fields{ 85 | Header: &HeaderChecks{ 86 | Jira: &JiraChecks{ 87 | Keys: []string{"JIRA", "PROJ"}, 88 | }, 89 | }, 90 | msg: "fix: [FALSE-1234] valid commit", 91 | }, 92 | want: want{errorCount: 1}, 93 | }, 94 | { 95 | name: "Invalid jira issue number", 96 | fields: fields{ 97 | Header: &HeaderChecks{ 98 | Jira: &JiraChecks{ 99 | Keys: []string{"JIRA", "PROJ"}, 100 | }, 101 | }, 102 | msg: "fix: JIRA-0 valid commit", 103 | }, 104 | want: want{errorCount: 1}, 105 | }, 106 | { 107 | name: "Valid commit with scope", 108 | fields: fields{ 109 | Header: &HeaderChecks{ 110 | Jira: &JiraChecks{ 111 | Keys: []string{"JIRA", "PROJ"}, 112 | }, 113 | }, 114 | msg: "fix(test): [PROJ-1234] valid commit", 115 | }, 116 | want: want{errorCount: 0}, 117 | }, 118 | { 119 | name: "Valid commit without square brackets", 120 | fields: fields{ 121 | Header: &HeaderChecks{ 122 | Jira: &JiraChecks{ 123 | Keys: []string{"JIRA", "PROJ"}, 124 | }, 125 | }, 126 | msg: "fix: PROJ-1234 valid commit", 127 | }, 128 | want: want{errorCount: 0}, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | c := Commit{ 134 | SpellCheck: tt.fields.SpellCheck, 135 | Conventional: tt.fields.Conventional, 136 | Header: tt.fields.Header, 137 | Body: tt.fields.Body, 138 | DCO: tt.fields.DCO, 139 | GPG: &GPG{ 140 | Required: tt.fields.GPG, 141 | }, 142 | MaximumOfOneCommit: tt.fields.MaximumOfOneCommit, 143 | msg: tt.fields.msg, 144 | } 145 | got := c.ValidateJiraCheck() 146 | 147 | if len(got.Errors()) != tt.want.errorCount { 148 | t.Errorf("Wanted %d errors but got %d errors: %v", tt.want.errorCount, len(got.Errors()), got.Errors()) 149 | } 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/policy/commit/check_number_of_commits.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/siderolabs/conform/internal/git" 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // NumberOfCommits enforces a maximum number of charcters on the commit 17 | // header. 18 | // 19 | //nolint:govet 20 | type NumberOfCommits struct { 21 | ref string 22 | ahead int 23 | errors []error 24 | } 25 | 26 | // Name returns the name of the check. 27 | func (h NumberOfCommits) Name() string { 28 | return "Number of Commits" 29 | } 30 | 31 | // Message returns to check message. 32 | func (h NumberOfCommits) Message() string { 33 | if len(h.errors) != 0 { 34 | return h.errors[0].Error() 35 | } 36 | 37 | return fmt.Sprintf("HEAD is %d commit(s) ahead of %s", h.ahead, h.ref) 38 | } 39 | 40 | // Errors returns any violations of the check. 41 | func (h NumberOfCommits) Errors() []error { 42 | return h.errors 43 | } 44 | 45 | // ValidateNumberOfCommits checks the header length. 46 | func (c Commit) ValidateNumberOfCommits(g *git.Git, ref string) policy.Check { //nolint:ireturn 47 | check := &NumberOfCommits{ 48 | ref: ref, 49 | } 50 | 51 | var err error 52 | 53 | check.ahead, _, err = g.AheadBehind(ref) 54 | if err != nil { 55 | check.errors = append(check.errors, err) 56 | 57 | return check 58 | } 59 | 60 | if check.ahead > 1 { 61 | check.errors = append(check.errors, errors.Errorf("HEAD is %d commit(s) ahead of %s", check.ahead, ref)) 62 | 63 | return check 64 | } 65 | 66 | return check 67 | } 68 | -------------------------------------------------------------------------------- /internal/policy/commit/check_spelling.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package commit 6 | 7 | import ( 8 | "fmt" 9 | "strings" 10 | 11 | "github.com/golangci/misspell" 12 | 13 | "github.com/siderolabs/conform/internal/policy" 14 | ) 15 | 16 | // SpellCheck represents to spell check policy. 17 | type SpellCheck struct { 18 | Locale string `mapstructure:"locale"` 19 | } 20 | 21 | // SpellingCheck enforces correct spelling. 22 | type SpellingCheck struct { 23 | errors []error 24 | } 25 | 26 | // Name returns the name of the check. 27 | func (h SpellingCheck) Name() string { 28 | return "Spellcheck" 29 | } 30 | 31 | // Message returns to check message. 32 | func (h SpellingCheck) Message() string { 33 | return fmt.Sprintf("Commit contains %d misspellings", len(h.errors)) 34 | } 35 | 36 | // Errors returns any violations of the check. 37 | func (h SpellingCheck) Errors() []error { 38 | return h.errors 39 | } 40 | 41 | // ValidateSpelling checks the spelling. 42 | func (c Commit) ValidateSpelling() policy.Check { //nolint:ireturn 43 | check := &SpellingCheck{} 44 | 45 | r := misspell.Replacer{ 46 | Replacements: misspell.DictMain, 47 | } 48 | 49 | switch strings.ToUpper(c.SpellCheck.Locale) { 50 | case "": 51 | case "US": 52 | r.AddRuleList(misspell.DictAmerican) 53 | case "UK", "GB": 54 | r.AddRuleList(misspell.DictBritish) 55 | case "NZ", "AU", "CA": 56 | check.errors = append(check.errors, fmt.Errorf("unknown locale: %q", c.SpellCheck.Locale)) 57 | } 58 | 59 | r.Compile() 60 | 61 | _, diffs := r.Replace(c.msg) 62 | 63 | for _, diff := range diffs { 64 | check.errors = append(check.errors, fmt.Errorf("`%s` is a misspelling of `%s`", diff.Original, diff.Corrected)) 65 | } 66 | 67 | return check 68 | } 69 | -------------------------------------------------------------------------------- /internal/policy/commit/commit.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package commit provides commit-related policies. 6 | package commit 7 | 8 | import ( 9 | "os" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "github.com/siderolabs/conform/internal/git" 16 | "github.com/siderolabs/conform/internal/policy" 17 | ) 18 | 19 | // HeaderChecks is the configuration for checks on the header of a commit. 20 | // 21 | //nolint:govet 22 | type HeaderChecks struct { 23 | // Length is the maximum length of the commit subject. 24 | Length int `mapstructure:"length"` 25 | // Imperative enforces the use of imperative verbs as the first word of a 26 | // commit message. 27 | Imperative bool `mapstructure:"imperative"` 28 | // HeaderCase is the case that the first word of the header must have ("upper" or "lower"). 29 | Case string `mapstructure:"case"` 30 | // HeaderInvalidLastCharacters is a string containing all invalid last characters for the header. 31 | InvalidLastCharacters string `mapstructure:"invalidLastCharacters"` 32 | // Jira checks if the header containers a Jira project key. 33 | Jira *JiraChecks `mapstructure:"jira"` 34 | } 35 | 36 | // JiraChecks is the configuration for checks for Jira issues. 37 | type JiraChecks struct { 38 | Keys []string `mapstructure:"keys"` 39 | } 40 | 41 | // BodyChecks is the configuration for checks on the body of a commit. 42 | type BodyChecks struct { 43 | // Required enforces that the current commit has a body. 44 | Required bool `mapstructure:"required"` 45 | } 46 | 47 | // GPG is the configuration for checks GPG signature on the commit. 48 | // 49 | //nolint:govet 50 | type GPG struct { 51 | // Required enforces that the current commit has a signature. 52 | Required bool `mapstructure:"required"` 53 | // Identity configures identity of the signature. 54 | Identity *struct { 55 | // GitHubOrganization enforces that commit should be signed with the key 56 | // of one of the organization public members. 57 | GitHubOrganization string `mapstructure:"gitHubOrganization"` 58 | } `mapstructure:"identity"` 59 | } 60 | 61 | // Commit implements the policy.Policy interface and enforces commit 62 | // messages to conform the Conventional Commit standard. 63 | // 64 | //nolint:maligned,govet 65 | type Commit struct { 66 | // SpellCheck enforces correct spelling. 67 | SpellCheck *SpellCheck `mapstructure:"spellcheck"` 68 | // Conventional is the user specified settings for conventional commits. 69 | Conventional *Conventional `mapstructure:"conventional"` 70 | // Header is the user specified settings for the header of each commit. 71 | Header *HeaderChecks `mapstructure:"header"` 72 | // Header is the user specified settings for the body of each commit. 73 | Body *BodyChecks `mapstructure:"body"` 74 | // DCO enables the Developer Certificate of Origin check. 75 | DCO bool `mapstructure:"dco"` 76 | // GPG is the user specified settings for the GPG signature check. 77 | GPG *GPG `mapstructure:"gpg"` 78 | // GPGSignatureGitHubOrganization enforces that GPG signature should come from 79 | // one of the members of the GitHub org. 80 | GPGSignatureGitHubOrganization string `mapstructure:"gpgSignatureGitHubOrg"` 81 | // MaximumOfOneCommit enforces that the current commit is only one commit 82 | // ahead of a specified ref. 83 | MaximumOfOneCommit bool `mapstructure:"maximumOfOneCommit"` 84 | 85 | msg string 86 | } 87 | 88 | // FirstWordRegex is theregular expression used to find the first word in a 89 | // commit. 90 | var FirstWordRegex = regexp.MustCompile(`^\s*([a-zA-Z0-9]+)`) 91 | 92 | // Compliance implements the policy.Policy.Compliance function. 93 | func (c *Commit) Compliance(options *policy.Options) (*policy.Report, error) { 94 | var err error 95 | 96 | report := &policy.Report{} 97 | 98 | // Setup the policy for all checks. 99 | var g *git.Git 100 | 101 | if g, err = git.NewGit(); err != nil { 102 | return report, errors.Errorf("failed to open git repo: %v", err) 103 | } 104 | 105 | var msgs []string 106 | 107 | switch o := options; { 108 | case o.CommitMsgFile != nil: 109 | var contents []byte 110 | 111 | if contents, err = os.ReadFile(*options.CommitMsgFile); err != nil { 112 | return report, errors.Errorf("failed to read commit message file: %v", err) 113 | } 114 | 115 | msgs = append(msgs, string(contents)) 116 | case o.RevisionRange != "": 117 | revs, err := extractRevisionRange(options) 118 | if err != nil { 119 | return report, errors.Errorf("failed to get commit message: %v", err) 120 | } 121 | 122 | msgs, err = g.Messages(revs[0], revs[1]) 123 | if err != nil { 124 | return report, errors.Errorf("failed to get commit message: %v", err) 125 | } 126 | default: 127 | msg, err := g.Message() 128 | if err != nil { 129 | return report, errors.Errorf("failed to get commit message: %v", err) 130 | } 131 | 132 | msgs = append(msgs, msg) 133 | } 134 | 135 | for i := range msgs { 136 | c.msg = msgs[i] 137 | 138 | c.compliance(report, g, options) 139 | } 140 | 141 | return report, nil 142 | } 143 | 144 | // compliance checks the compliance with the policies of the given commit. 145 | func (c *Commit) compliance(report *policy.Report, g *git.Git, options *policy.Options) { 146 | if c.Header != nil { 147 | if c.Header.Length != 0 { 148 | report.AddCheck(c.ValidateHeaderLength()) 149 | } 150 | 151 | if c.Header.Imperative { 152 | report.AddCheck(c.ValidateImperative()) 153 | } 154 | 155 | if c.Header.Case != "" { 156 | report.AddCheck(c.ValidateHeaderCase()) 157 | } 158 | 159 | if c.Header.InvalidLastCharacters != "" { 160 | report.AddCheck(c.ValidateHeaderLastCharacter()) 161 | } 162 | 163 | if c.Header.Jira != nil { 164 | report.AddCheck(c.ValidateJiraCheck()) 165 | } 166 | } 167 | 168 | if c.DCO { 169 | report.AddCheck(c.ValidateDCO()) 170 | } 171 | 172 | if c.GPG != nil { 173 | if c.GPG.Required { 174 | report.AddCheck(c.ValidateGPGSign(g)) 175 | 176 | if c.GPG.Identity != nil { 177 | report.AddCheck(c.ValidateGPGIdentity(g)) 178 | } 179 | } 180 | } 181 | 182 | if c.Conventional != nil { 183 | report.AddCheck(c.ValidateConventionalCommit()) 184 | } 185 | 186 | if c.SpellCheck != nil { 187 | report.AddCheck(c.ValidateSpelling()) 188 | } 189 | 190 | if c.MaximumOfOneCommit { 191 | report.AddCheck(c.ValidateNumberOfCommits(g, options.CommitRef)) 192 | } 193 | 194 | if c.Body != nil { 195 | if c.Body.Required { 196 | report.AddCheck(c.ValidateBody()) 197 | } 198 | } 199 | } 200 | 201 | func (c Commit) firstWord() (string, error) { 202 | var ( 203 | groups []string 204 | msg string 205 | ) 206 | 207 | if c.Conventional != nil { 208 | groups = parseHeader(c.msg) 209 | if len(groups) != 7 { 210 | return "", errors.Errorf("Invalid conventional commit format") 211 | } 212 | 213 | msg = groups[5] 214 | } else { 215 | msg = c.msg 216 | } 217 | 218 | if msg == "" { 219 | return "", errors.Errorf("Invalid msg: %s", msg) 220 | } 221 | 222 | if groups = FirstWordRegex.FindStringSubmatch(msg); groups == nil { 223 | return "", errors.Errorf("Invalid msg: %s", msg) 224 | } 225 | 226 | return groups[0], nil 227 | } 228 | 229 | func (c Commit) header() string { 230 | return strings.Split(strings.TrimPrefix(c.msg, "\n"), "\n")[0] 231 | } 232 | 233 | func extractRevisionRange(options *policy.Options) ([]string, error) { 234 | revs := strings.Split(options.RevisionRange, "..") 235 | if len(revs) > 2 || len(revs) == 0 || revs[0] == "" || revs[1] == "" { 236 | return nil, errors.New("invalid revision range") 237 | } else if len(revs) == 1 { 238 | // if no final rev is given, use HEAD as default 239 | revs = append(revs, "HEAD") 240 | } 241 | 242 | return revs, nil 243 | } 244 | -------------------------------------------------------------------------------- /internal/policy/commit/commit_test.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | //nolint:testpackage 6 | package commit 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/siderolabs/conform/internal/policy" 16 | ) 17 | 18 | //nolint:gocognit 19 | func TestConventionalCommitPolicy(t *testing.T) { 20 | //nolint:govet 21 | type testDesc struct { 22 | Name string 23 | CreateCommit func() error 24 | ExpectValid bool 25 | } 26 | 27 | for _, test := range []testDesc{ 28 | { 29 | Name: "Valid", 30 | CreateCommit: createValidScopedCommit, 31 | ExpectValid: true, 32 | }, 33 | { 34 | Name: "ValidBreaking", 35 | CreateCommit: createValidBreakingCommit, 36 | ExpectValid: true, 37 | }, 38 | { 39 | Name: "InvalidBreakingSymbol", 40 | CreateCommit: createInvalidBreakingSymbolCommit, 41 | ExpectValid: false, 42 | }, 43 | { 44 | Name: "ValidScopedBreaking", 45 | CreateCommit: createValidScopedBreakingCommit, 46 | ExpectValid: true, 47 | }, 48 | { 49 | Name: "InvalidScopedBreaking", 50 | CreateCommit: createInvalidScopedBreakingCommit, 51 | ExpectValid: false, 52 | }, 53 | { 54 | Name: "Invalid", 55 | CreateCommit: createInvalidCommit, 56 | ExpectValid: false, 57 | }, 58 | { 59 | Name: "InvalidEmpty", 60 | CreateCommit: createInvalidEmptyCommit, 61 | ExpectValid: false, 62 | }, 63 | } { 64 | func(test testDesc) { 65 | t.Run(test.Name, func(tt *testing.T) { 66 | dir := t.TempDir() 67 | 68 | err := os.Chdir(dir) 69 | if err != nil { 70 | tt.Error(err) 71 | } 72 | 73 | err = initRepo() 74 | if err != nil { 75 | tt.Error(err) 76 | } 77 | 78 | err = test.CreateCommit() 79 | if err != nil { 80 | tt.Error(err) 81 | } 82 | 83 | report, err := runCompliance() 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | 88 | if test.ExpectValid { 89 | if !report.Valid() { 90 | tt.Error("Report is invalid with valid conventional commit") 91 | } 92 | } else { 93 | if report.Valid() { 94 | tt.Error("Report is valid with invalid conventional commit") 95 | } 96 | } 97 | }) 98 | }(test) 99 | } 100 | } 101 | 102 | func TestValidateDCO(t *testing.T) { 103 | type testDesc struct { 104 | Name string 105 | CommitMessage string 106 | ExpectValid bool 107 | } 108 | 109 | for _, test := range []testDesc{ 110 | { 111 | Name: "Valid DCO", 112 | CommitMessage: "something nice\n\nSigned-off-by: Foo Bar \n\n", 113 | ExpectValid: true, 114 | }, 115 | { 116 | Name: "Valid DCO with CRLF", 117 | CommitMessage: "something nice\r\n\r\nSigned-off-by: Foo Bar \r\n\r\n", 118 | ExpectValid: true, 119 | }, 120 | { 121 | Name: "No DCO", 122 | CommitMessage: "something nice\n\nnot signed\n", 123 | ExpectValid: false, 124 | }, 125 | } { 126 | // Fixes scopelint error. 127 | t.Run(test.Name, func(tt *testing.T) { 128 | var report policy.Report 129 | 130 | c := Commit{msg: test.CommitMessage} 131 | report.AddCheck(c.ValidateDCO()) 132 | 133 | if test.ExpectValid { 134 | if !report.Valid() { 135 | tt.Error("Report is invalid with valid DCP") 136 | } 137 | } else { 138 | if report.Valid() { 139 | tt.Error("Report is valid with invalid DCO") 140 | } 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func TestValidConventionalCommitPolicy(t *testing.T) { 147 | dir := t.TempDir() 148 | 149 | err := os.Chdir(dir) 150 | if err != nil { 151 | t.Error(err) 152 | } 153 | 154 | err = initRepo() 155 | if err != nil { 156 | t.Error(err) 157 | } 158 | 159 | err = createValidScopedCommit() 160 | if err != nil { 161 | t.Error(err) 162 | } 163 | 164 | report, err := runCompliance() 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | 169 | if !report.Valid() { 170 | t.Errorf("Report is invalid with valid conventional commit") 171 | } 172 | } 173 | 174 | func TestInvalidConventionalCommitPolicy(t *testing.T) { 175 | dir := t.TempDir() 176 | 177 | err := os.Chdir(dir) 178 | if err != nil { 179 | t.Error(err) 180 | } 181 | 182 | err = initRepo() 183 | if err != nil { 184 | t.Error(err) 185 | } 186 | 187 | err = createInvalidCommit() 188 | if err != nil { 189 | t.Error(err) 190 | } 191 | 192 | report, err := runCompliance() 193 | if err != nil { 194 | t.Error(err) 195 | } 196 | 197 | if report.Valid() { 198 | t.Errorf("Report is valid with invalid conventional commit") 199 | } 200 | } 201 | 202 | func TestEmptyConventionalCommitPolicy(t *testing.T) { 203 | dir := t.TempDir() 204 | 205 | err := os.Chdir(dir) 206 | if err != nil { 207 | t.Error(err) 208 | } 209 | 210 | err = initRepo() 211 | if err != nil { 212 | t.Error(err) 213 | } 214 | 215 | err = createInvalidEmptyCommit() 216 | if err != nil { 217 | t.Error(err) 218 | } 219 | 220 | report, err := runCompliance() 221 | if err != nil { 222 | t.Error(err) 223 | } 224 | 225 | if report.Valid() { 226 | t.Error("Report is valid with invalid conventional commit") 227 | } 228 | } 229 | 230 | func TestValidConventionalCommitPolicyRegex(t *testing.T) { 231 | dir := t.TempDir() 232 | 233 | err := os.Chdir(dir) 234 | if err != nil { 235 | t.Error(err) 236 | } 237 | 238 | err = initRepo() 239 | if err != nil { 240 | t.Error(err) 241 | } 242 | 243 | err = createValidCommitRegex() 244 | if err != nil { 245 | t.Error(err) 246 | } 247 | 248 | report, err := runCompliance() 249 | if err != nil { 250 | t.Error(err) 251 | } 252 | 253 | if !report.Valid() { 254 | t.Error("Report is invalid with valid conventional commit") 255 | } 256 | } 257 | 258 | func TestInvalidConventionalCommitPolicyRegex(t *testing.T) { 259 | dir := t.TempDir() 260 | 261 | err := os.Chdir(dir) 262 | if err != nil { 263 | t.Error(err) 264 | } 265 | 266 | err = initRepo() 267 | if err != nil { 268 | t.Error(err) 269 | } 270 | 271 | err = createInvalidCommitRegex() 272 | if err != nil { 273 | t.Error(err) 274 | } 275 | 276 | report, err := runCompliance() 277 | if err != nil { 278 | t.Error(err) 279 | } 280 | 281 | if report.Valid() { 282 | t.Error("Report is valid with invalid conventional commit") 283 | } 284 | } 285 | 286 | func TestValidRevisionRange(t *testing.T) { 287 | dir := t.TempDir() 288 | 289 | err := os.Chdir(dir) 290 | if err != nil { 291 | t.Error(err) 292 | } 293 | 294 | err = initRepo() 295 | if err != nil { 296 | t.Error(err) 297 | } 298 | 299 | revs, err := createValidCommitRange() 300 | if err != nil { 301 | t.Fatal(err) 302 | } 303 | 304 | // Test with a valid revision range 305 | report, err := runComplianceRange(revs[0], revs[len(revs)-1]) 306 | if err != nil { 307 | t.Error(err) 308 | } 309 | 310 | if !report.Valid() { 311 | t.Error("Report is invalid with valid conventional commits") 312 | } 313 | 314 | // Test with HEAD as end of revision range 315 | report, err = runComplianceRange(revs[0], "HEAD") 316 | if err != nil { 317 | t.Error(err) 318 | } 319 | 320 | if !report.Valid() { 321 | t.Error("Report is invalid with valid conventional commits") 322 | } 323 | 324 | // Test with empty end of revision range (should fail) 325 | _, err = runComplianceRange(revs[0], "") 326 | if err == nil { 327 | t.Error("Invalid end of revision, got success, expecting failure") 328 | } 329 | 330 | // Test with empty start of revision (should fail) 331 | _, err = runComplianceRange("", "HEAD") 332 | if err == nil { 333 | t.Error("Invalid end of revision, got success, expecting failure") 334 | } 335 | 336 | // Test with start of revision not an ancestor of end of range (should fail) 337 | _, err = runComplianceRange(revs[1], revs[0]) 338 | if err == nil { 339 | t.Error("Invalid end of revision, got success, expecting failure") 340 | } 341 | } 342 | 343 | func createValidCommitRange() ([]string, error) { 344 | revs := make([]string, 0, 4) 345 | 346 | for i := range 4 { 347 | err := os.WriteFile("test", []byte(fmt.Sprint(i)), 0o644) 348 | if err != nil { 349 | return nil, fmt.Errorf("writing test file failed: %w", err) 350 | } 351 | 352 | _, err = exec.Command("git", "add", "test").Output() 353 | if err != nil { 354 | return nil, fmt.Errorf("git add failed: %w", err) 355 | } 356 | 357 | _, err = exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", fmt.Sprintf("type(scope): description %d", i)).Output() 358 | if err != nil { 359 | return nil, fmt.Errorf("git commit failed: %w", err) 360 | } 361 | 362 | id, err := exec.Command("git", "rev-parse", "HEAD").Output() 363 | if err != nil { 364 | return nil, fmt.Errorf("rev-parse failed: %w", err) 365 | } 366 | 367 | revs = append(revs, strings.TrimSpace(string(id))) 368 | } 369 | 370 | return revs, nil 371 | } 372 | 373 | func runComplianceRange(id1, id2 string) (*policy.Report, error) { 374 | c := &Commit{ 375 | Conventional: &Conventional{ 376 | Types: []string{"type"}, 377 | Scopes: []string{"scope", "^valid"}, 378 | }, 379 | } 380 | 381 | return c.Compliance(&policy.Options{ 382 | RevisionRange: fmt.Sprintf("%s..%s", id1, id2), 383 | }) 384 | } 385 | 386 | func runCompliance() (*policy.Report, error) { 387 | c := &Commit{ 388 | Conventional: &Conventional{ 389 | Types: []string{"type"}, 390 | Scopes: []string{"scope", "^valid"}, 391 | }, 392 | } 393 | 394 | return c.Compliance(&policy.Options{}) 395 | } 396 | 397 | func initRepo() error { 398 | _, err := exec.Command("git", "init").Output() 399 | if err != nil { 400 | return err 401 | } 402 | 403 | _, err = exec.Command("touch", "test").Output() 404 | if err != nil { 405 | return err 406 | } 407 | 408 | _, err = exec.Command("git", "add", "test").Output() 409 | 410 | return err 411 | } 412 | 413 | func createValidScopedCommit() error { 414 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "type(scope): description").Output() 415 | 416 | return err 417 | } 418 | 419 | func createValidBreakingCommit() error { 420 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "feat!: description").Output() 421 | 422 | return err 423 | } 424 | 425 | func createInvalidBreakingSymbolCommit() error { 426 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "feat$: description").Output() 427 | 428 | return err 429 | } 430 | 431 | func createValidScopedBreakingCommit() error { 432 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "feat(scope)!: description").Output() 433 | 434 | return err 435 | } 436 | 437 | func createInvalidScopedBreakingCommit() error { 438 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "feat!(scope): description").Output() 439 | 440 | return err 441 | } 442 | 443 | func createInvalidCommit() error { 444 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "invalid commit").Output() 445 | 446 | return err 447 | } 448 | 449 | func createInvalidEmptyCommit() error { 450 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "--allow-empty-message", "-m", "").Output() 451 | 452 | return err 453 | } 454 | 455 | func createValidCommitRegex() error { 456 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "type(valid-1): description").Output() 457 | 458 | return err 459 | } 460 | 461 | func createInvalidCommitRegex() error { 462 | _, err := exec.Command("git", "-c", "user.name='test'", "-c", "user.email='test@siderolabs.io'", "commit", "-m", "type(invalid-1): description").Output() 463 | 464 | return err 465 | } 466 | -------------------------------------------------------------------------------- /internal/policy/license/license.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package license provides license policy. 6 | package license 7 | 8 | import ( 9 | "bufio" 10 | "bytes" 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/denormal/go-gitignore" 17 | "github.com/pkg/errors" 18 | 19 | "github.com/siderolabs/conform/internal/policy" 20 | ) 21 | 22 | // Licenses implement the policy.Policy interface and enforces source code license headers. 23 | type Licenses []License 24 | 25 | // License represents a single license policy. 26 | // 27 | //nolint:govet 28 | type License struct { 29 | Root string `mapstructure:"root"` 30 | // SkipPaths applies gitignore-style patterns to file paths to skip completely 31 | // parts of the tree which shouldn't be scanned (e.g. .git/) 32 | SkipPaths []string `mapstructure:"skipPaths"` 33 | // IncludeSuffixes is the regex used to find files that the license policy 34 | // should be applied to. 35 | IncludeSuffixes []string `mapstructure:"includeSuffixes"` 36 | // ExcludeSuffixes is the Suffixes used to find files that the license policy 37 | // should not be applied to. 38 | ExcludeSuffixes []string `mapstructure:"excludeSuffixes"` 39 | // AllowPrecedingComments, when enabled, allows blank lines and `//` and `#` line comments 40 | // before the license header. Useful for code generators that put build constraints or 41 | // "DO NOT EDIT" lines before the license. 42 | AllowPrecedingComments bool `mapstructure:"allowPrecedingComments"` 43 | // Header is the contents of the license header. 44 | Header string `mapstructure:"header"` 45 | } 46 | 47 | // Compliance implements the policy.Policy.Compliance function. 48 | func (l *Licenses) Compliance(_ *policy.Options) (*policy.Report, error) { 49 | report := &policy.Report{} 50 | 51 | report.AddCheck(l.ValidateLicenseHeaders()) 52 | 53 | return report, nil 54 | } 55 | 56 | // HeaderCheck enforces a license header on source code files. 57 | type HeaderCheck struct { 58 | licenseErrors []error 59 | } 60 | 61 | // Name returns the name of the check. 62 | func (l HeaderCheck) Name() string { 63 | return "File Header" 64 | } 65 | 66 | // Message returns to check message. 67 | func (l HeaderCheck) Message() string { 68 | if len(l.licenseErrors) != 0 { 69 | return fmt.Sprintf("Found %d files without license header", len(l.licenseErrors)) 70 | } 71 | 72 | return "All files have a valid license header" 73 | } 74 | 75 | // Errors returns any violations of the check. 76 | func (l HeaderCheck) Errors() []error { 77 | return l.licenseErrors 78 | } 79 | 80 | // ValidateLicenseHeaders checks the header of a file and ensures it contains the provided value. 81 | func (l Licenses) ValidateLicenseHeaders() policy.Check { //nolint:ireturn 82 | check := HeaderCheck{} 83 | 84 | for _, license := range l { 85 | if license.Root == "" { 86 | license.Root = "." 87 | } 88 | 89 | check.licenseErrors = append(check.licenseErrors, validateLicenseHeader(license)...) 90 | } 91 | 92 | return check 93 | } 94 | 95 | //nolint:gocognit 96 | func validateLicenseHeader(license License) []error { 97 | var errs []error 98 | 99 | var buf bytes.Buffer 100 | 101 | for _, pattern := range license.SkipPaths { 102 | fmt.Fprintf(&buf, "%s\n", pattern) 103 | } 104 | 105 | patternmatcher := gitignore.New(&buf, license.Root, func(e gitignore.Error) bool { 106 | errs = append(errs, e.Underlying()) 107 | 108 | return true 109 | }) 110 | 111 | if license.Header == "" { 112 | errs = append(errs, errors.New("Header is not defined")) 113 | 114 | return errs 115 | } 116 | 117 | value := []byte(strings.TrimSpace(license.Header)) 118 | 119 | err := filepath.Walk(license.Root, func(path string, info os.FileInfo, err error) error { 120 | if err != nil { 121 | return err 122 | } 123 | 124 | if patternmatcher.Relative(path, info.IsDir()) != nil { 125 | if info.IsDir() { 126 | if info.IsDir() { 127 | // skip whole directory tree 128 | return filepath.SkipDir 129 | } 130 | // skip single file 131 | return nil 132 | } 133 | } 134 | 135 | if info.Mode().IsRegular() { 136 | // Skip excluded suffixes. 137 | for _, suffix := range license.ExcludeSuffixes { 138 | if strings.HasSuffix(info.Name(), suffix) { 139 | return nil 140 | } 141 | } 142 | 143 | // Check files matching the included suffixes. 144 | for _, suffix := range license.IncludeSuffixes { 145 | if strings.HasSuffix(info.Name(), suffix) { 146 | if license.AllowPrecedingComments { 147 | err = validateFileWithPrecedingComments(path, value) 148 | } else { 149 | err = validateFile(path, value) 150 | } 151 | 152 | if err != nil { 153 | errs = append(errs, err) 154 | } 155 | } 156 | } 157 | } 158 | 159 | return nil 160 | }) 161 | if err != nil { 162 | errs = append(errs, errors.Errorf("Failed to walk directory: %v", err)) 163 | } 164 | 165 | return errs 166 | } 167 | 168 | func validateFile(path string, value []byte) error { 169 | contents, err := os.ReadFile(path) 170 | if err != nil { 171 | return errors.Errorf("Failed to read %s: %s", path, err) 172 | } 173 | 174 | if bytes.HasPrefix(contents, value) { 175 | return nil 176 | } 177 | 178 | return errors.Errorf("File %s does not contain a license header", path) 179 | } 180 | 181 | func validateFileWithPrecedingComments(path string, value []byte) error { 182 | f, err := os.Open(path) 183 | if err != nil { 184 | return errors.Errorf("Failed to open %s: %s", path, err) 185 | } 186 | defer f.Close() //nolint:errcheck 187 | 188 | var contents []byte 189 | 190 | // read lines until the first non-comment line 191 | scanner := bufio.NewScanner(f) 192 | for scanner.Scan() { 193 | line := strings.TrimSpace(scanner.Text()) 194 | 195 | comment := line == "" 196 | comment = comment || strings.HasPrefix(line, "//") 197 | comment = comment || strings.HasPrefix(line, "#") 198 | 199 | if !comment { 200 | break 201 | } 202 | 203 | contents = append(contents, scanner.Bytes()...) 204 | contents = append(contents, '\n') 205 | } 206 | 207 | if err := scanner.Err(); err != nil { 208 | return errors.Errorf("Failed to check file %s: %s", path, err) 209 | } 210 | 211 | if bytes.Contains(contents, value) { 212 | return nil 213 | } 214 | 215 | return errors.Errorf("File %s does not contain a license header", path) 216 | } 217 | -------------------------------------------------------------------------------- /internal/policy/license/license_test.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | //go:build !some_test_tag 6 | // +build !some_test_tag 7 | 8 | package license_test 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/siderolabs/conform/internal/policy/license" 16 | ) 17 | 18 | func TestLicense(t *testing.T) { 19 | const header = ` 20 | // This Source Code Form is subject to the terms of the Mozilla Public 21 | // License, v. 2.0. If a copy of the MPL was not distributed with this 22 | // file, You can obtain one at http://mozilla.org/MPL/2.0/.` 23 | 24 | const otherHeader = "// some-other-header" 25 | 26 | t.Run("Default", func(t *testing.T) { 27 | l := license.Licenses{ 28 | { 29 | SkipPaths: []string{"subdir1/"}, 30 | IncludeSuffixes: []string{".txt"}, 31 | AllowPrecedingComments: false, 32 | Header: header, 33 | }, 34 | } 35 | check := l.ValidateLicenseHeaders() 36 | assert.Equal(t, "Found 1 files without license header", check.Message()) 37 | }) 38 | 39 | t.Run("AllowPrecedingComments", func(t *testing.T) { 40 | l := license.Licenses{ 41 | { 42 | SkipPaths: []string{"subdir1/"}, 43 | IncludeSuffixes: []string{".txt"}, 44 | AllowPrecedingComments: true, 45 | Header: header, 46 | }, 47 | } 48 | check := l.ValidateLicenseHeaders() 49 | assert.Equal(t, "All files have a valid license header", check.Message()) 50 | }) 51 | 52 | // File "testdata/subdir1/subdir2/data.txt" is valid for the root license, but "testdata/subdir1/" is skipped. 53 | // It is invalid for the additional license, but that license skips "subdir2/" relative to itself. 54 | // The check should pass. 55 | t.Run("AdditionalValid", func(t *testing.T) { 56 | l := license.Licenses{ 57 | { 58 | IncludeSuffixes: []string{".txt"}, 59 | SkipPaths: []string{"testdata/subdir1/"}, 60 | AllowPrecedingComments: true, 61 | Header: header, 62 | }, 63 | { 64 | Root: "testdata/subdir1/", 65 | SkipPaths: []string{"subdir2/"}, 66 | IncludeSuffixes: []string{".txt"}, 67 | Header: otherHeader, 68 | }, 69 | } 70 | check := l.ValidateLicenseHeaders() 71 | assert.Equal(t, "All files have a valid license header", check.Message()) 72 | }) 73 | 74 | // File "testdata/subdir1/subdir2/data.txt" is valid for the root license, but "testdata/subdir1/" is skipped. 75 | // However, it is invalid for the additional license. 76 | // The check should fail. 77 | t.Run("AdditionalInvalid", func(t *testing.T) { 78 | l := license.Licenses{ 79 | { 80 | IncludeSuffixes: []string{".txt"}, 81 | SkipPaths: []string{"testdata/subdir1/"}, 82 | AllowPrecedingComments: true, 83 | Header: header, 84 | }, 85 | 86 | { 87 | Root: "testdata/subdir1/", 88 | IncludeSuffixes: []string{".txt"}, 89 | Header: otherHeader, 90 | }, 91 | } 92 | check := l.ValidateLicenseHeaders() 93 | assert.Equal(t, "Found 1 files without license header", check.Message()) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /internal/policy/license/testdata/data.txt: -------------------------------------------------------------------------------- 1 | //this is a preceding comment 2 | 3 | // This Source Code Form is subject to the terms of the Mozilla Public 4 | // License, v. 2.0. If a copy of the MPL was not distributed with this 5 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 | -------------------------------------------------------------------------------- /internal/policy/license/testdata/subdir1/data.txt: -------------------------------------------------------------------------------- 1 | // some-other-header 2 | 3 | content 4 | -------------------------------------------------------------------------------- /internal/policy/license/testdata/subdir1/subdir2/data.txt: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | content 6 | -------------------------------------------------------------------------------- /internal/policy/policy.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package policy provides base policy definitions. 6 | package policy 7 | 8 | // Report reports the compliance of a policy. 9 | type Report struct { 10 | checks []Check 11 | } 12 | 13 | // Check defines a policy check. 14 | type Check interface { 15 | Name() string 16 | Message() string 17 | Errors() []error 18 | } 19 | 20 | // Policy is an interface that policies must implement. 21 | type Policy interface { 22 | Compliance(*Options) (*Report, error) 23 | } 24 | 25 | // Valid checks if a report is valid. 26 | func (r *Report) Valid() bool { 27 | for _, check := range r.checks { 28 | if len(check.Errors()) != 0 { 29 | return false 30 | } 31 | } 32 | 33 | return true 34 | } 35 | 36 | // Checks returns the checks executed by a policy. 37 | func (r *Report) Checks() []Check { 38 | return r.checks 39 | } 40 | 41 | // AddCheck adds a check to the policy report. 42 | func (r *Report) AddCheck(c Check) { 43 | r.checks = append(r.checks, c) 44 | } 45 | -------------------------------------------------------------------------------- /internal/policy/policy_options.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | package policy 6 | 7 | // Option is a functional option used to pass in arguments to a Policy. 8 | type Option func(*Options) 9 | 10 | // Options defines the set of options available to a Policy. 11 | type Options struct { 12 | CommitMsgFile *string 13 | CommitRef string 14 | RevisionRange string 15 | } 16 | 17 | // WithCommitMsgFile sets the path to the commit message file. 18 | func WithCommitMsgFile(o *string) Option { 19 | return func(args *Options) { 20 | args.CommitMsgFile = o 21 | } 22 | } 23 | 24 | // WithCommitRef sets the ref to compare git policies against. 25 | func WithCommitRef(o string) Option { 26 | return func(args *Options) { 27 | args.CommitRef = o 28 | } 29 | } 30 | 31 | // WithRevisionRange sets the revision range to compare git policies against. 32 | func WithRevisionRange(o string) Option { 33 | return func(args *Options) { 34 | args.RevisionRange = o 35 | } 36 | } 37 | 38 | // NewDefaultOptions initializes a Options struct with default values. 39 | func NewDefaultOptions(setters ...Option) *Options { 40 | opts := &Options{ 41 | CommitMsgFile: nil, 42 | } 43 | 44 | for _, setter := range setters { 45 | setter(opts) 46 | } 47 | 48 | return opts 49 | } 50 | -------------------------------------------------------------------------------- /internal/policy/version/version.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package version provides version policies. 6 | package version 7 | 8 | // Version defines the version policy to use and the options specific to the 9 | // policy. 10 | type Version struct{} 11 | -------------------------------------------------------------------------------- /internal/reporter/reporter.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // Package reporter provides check result reporting. 6 | package reporter 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "net/http" 14 | "os" 15 | "path" 16 | "strings" 17 | 18 | "github.com/google/go-github/v60/github" 19 | ) 20 | 21 | // Reporter describes a hook for sending summarized results to a remote API. 22 | type Reporter interface { 23 | SetStatus(string, string, string, string) error 24 | } 25 | 26 | // GitHub is a reporter that summarizes policy statuses as GitHub statuses. 27 | type GitHub struct { 28 | token string 29 | owner string 30 | repo string 31 | sha string 32 | } 33 | 34 | // Noop is a reporter that does nothing. 35 | type Noop struct{} 36 | 37 | // SetStatus is a noop func. 38 | func (n *Noop) SetStatus(_, _, _, _ string) error { 39 | return nil 40 | } 41 | 42 | // NewGitHubReporter returns a reporter that posts policy checks as 43 | // status checks on a pull request. 44 | func NewGitHubReporter() (*GitHub, error) { 45 | token, ok := os.LookupEnv("INPUT_TOKEN") 46 | if !ok { 47 | return nil, errors.New("missing INPUT_TOKEN") 48 | } 49 | 50 | eventPath, ok := os.LookupEnv("GITHUB_EVENT_PATH") 51 | if !ok { 52 | return nil, errors.New("GITHUB_EVENT_PATH is not set") 53 | } 54 | 55 | data, err := os.ReadFile(eventPath) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | pullRequestEvent := &github.PullRequestEvent{} 61 | 62 | if err = json.Unmarshal(data, pullRequestEvent); err != nil { 63 | return nil, err 64 | } 65 | 66 | gh := &GitHub{ 67 | token: token, 68 | owner: pullRequestEvent.GetRepo().GetOwner().GetLogin(), 69 | repo: pullRequestEvent.GetRepo().GetName(), 70 | sha: pullRequestEvent.GetPullRequest().GetHead().GetSHA(), 71 | } 72 | 73 | return gh, nil 74 | } 75 | 76 | // SetStatus sets the status of a GitHub check. 77 | // 78 | // Valid statuses are "error", "failure", "pending", "success". 79 | func (gh *GitHub) SetStatus(state, policy, check, message string) error { 80 | if gh.token == "" { 81 | return errors.New("no token") 82 | } 83 | 84 | statusCheckContext := strings.ReplaceAll(strings.ToLower(path.Join("conform", policy, check)), " ", "-") 85 | description := message 86 | repoStatus := &github.RepoStatus{} 87 | repoStatus.Context = &statusCheckContext 88 | repoStatus.Description = &description 89 | repoStatus.State = &state 90 | 91 | http.DefaultClient.Transport = roundTripper{gh.token} 92 | githubClient := github.NewClient(http.DefaultClient) 93 | 94 | _, _, err := githubClient.Repositories.CreateStatus(context.Background(), gh.owner, gh.repo, gh.sha, repoStatus) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | return nil 100 | } 101 | 102 | type roundTripper struct { 103 | accessToken string 104 | } 105 | 106 | // RoundTrip implements the net/http.RoundTripper interface. 107 | func (rt roundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 108 | r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", rt.accessToken)) 109 | 110 | return http.DefaultTransport.RoundTrip(r) 111 | } 112 | -------------------------------------------------------------------------------- /internal/version/data/sha: -------------------------------------------------------------------------------- 1 | undefined -------------------------------------------------------------------------------- /internal/version/data/tag: -------------------------------------------------------------------------------- 1 | v0.1.0-alpha.27 -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // This Source Code Form is subject to the terms of the Mozilla Public 2 | // License, v. 2.0. If a copy of the MPL was not distributed with this 3 | // file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | 5 | // THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. 6 | // 7 | // Generated on 2024-02-15T11:12:45Z by kres latest. 8 | 9 | // Package version contains variables such as project name, tag and sha. It's a proper alternative to using 10 | // -ldflags '-X ...'. 11 | package version 12 | 13 | import ( 14 | _ "embed" 15 | "runtime/debug" 16 | "strings" 17 | ) 18 | 19 | var ( 20 | // Tag declares project git tag. 21 | //go:embed data/tag 22 | Tag string 23 | // SHA declares project git SHA. 24 | //go:embed data/sha 25 | SHA string 26 | // Name declares project name. 27 | Name = func() string { 28 | info, ok := debug.ReadBuildInfo() 29 | if !ok { 30 | panic("cannot read build info, something is very wrong") 31 | } 32 | 33 | // Check if siderolabs project 34 | if strings.HasPrefix(info.Path, "github.com/siderolabs/") { 35 | return info.Path[strings.LastIndex(info.Path, "/")+1:] 36 | } 37 | 38 | // We could return a proper full path here, but it could be seen as a privacy violation. 39 | return "community-project" 40 | }() 41 | ) 42 | --------------------------------------------------------------------------------