├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
--------------------------------------------------------------------------------