Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.8
python-version: "3.14"
- name: Set up chart-testing
uses: helm/chart-testing-action@v2.7.0
uses: helm/chart-testing-action@v2.9.0
- name: Run chart-testing (list-changed)
id: list-changed
run: |
Expand Down
81 changes: 81 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,84 @@ jobs:
toolchain: 1.99.1
- name: Run backend tests
run: cargo test

tests-chart-config:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.99.1
- name: Setup Helm
uses: azure/setup-helm@v4
with:
version: v3.9.2
- name: Check the configuration rendered by the chart
run: cargo test --test chart_config -- --ignored

tests-database:
runs-on: ubuntu-latest
env:
GITVOTE_TEST_DB_URL: postgres://postgres:postgres@127.0.0.1:5432/gitvote_tests
PGDATABASE: gitvote_tests
PGHOST: 127.0.0.1
PGPASSWORD: postgres
PGUSER: postgres
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: gitvote_tests
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.99.1
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version: stable
cache: false
- name: Install tern
run: go install github.com/jackc/tern/v2@v2.4.3
- name: Install pgTAP in the PostgreSQL service
run: |
docker exec ${{ job.services.postgres.id }} sh -c \
"apt-get update && apt-get install -y postgresql-17-pgtap"
- name: Install pg_prove
run: |
sudo apt-get update
sudo apt-get install -y libtap-parser-sourcehandler-pgtap-perl
- name: Apply database migrations
working-directory: database/migrations
run: |
cat > "$RUNNER_TEMP/tern.conf" <<EOF
[database]
host = 127.0.0.1
port = 5432
database = gitvote_tests
user = postgres
password = postgres
sslmode = disable
EOF
export TERN_CONF="$RUNNER_TEMP/tern.conf"
./migrate.sh
./migrate.sh
- name: Run pgTAP tests
run: |
psql -c "create extension if not exists pgtap"
pg_prove --ext .sql --recurse database/tests
- name: Run database contract tests
run: cargo test db_contracts -- --ignored
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ octocrab = { version = "0.53.1", default-features = false, features = [
"timeout",
"tracing",
] }
percent-encoding = "2.3.2"
regex = "1.13.1"
reqwest = "0.13.5"
rustls = "0.23.46"
Expand Down Expand Up @@ -74,3 +75,4 @@ http-body = "1.1.0"
hyper = "1.12.0"
mockall = "0.15.0"
proptest = { version = "1.12.0", default-features = false, features = ["std"] }
tokio = { version = "1.54.1", features = ["test-util"] }
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,26 @@ The command **must** be on a line by itself. Please note that GitVote only detec

![create-vote](docs/screenshots/create-vote.png)

Alternatively, if you have setup multiple configuration profiles, you can also start votes using any of them with the command `/vote-PROFILE`. The profile name can contain letters, digits, hyphens and underscores.
Alternatively, if you have setup multiple configuration profiles, you can also start votes using any of them with the command `/vote-PROFILE`. The profile name can contain letters, digits, hyphens and underscores, and it must follow the hyphen: lines like `/votes` or `/vote-` are not commands.

> [!NOTE]
> Only repositories collaborators can create votes. For organization-owned repositories, the list of collaborators includes outside collaborators, organization members that are direct collaborators, organization members with access through team memberships, organization members with access through default organization permissions, and organization owners.

Shortly after the comment with the `/vote` command is posted, the vote will be created and the bot will post a new comment to the corresponding issue or pull request with the vote instructions.

The `duration` of a vote is set in the configuration profile used. It must be at least 1 second, and it cannot be greater than 36500 days. Otherwise no vote is created and the bot posts a comment saying the configuration is invalid.

![vote-created](docs/screenshots/vote-created.png)

#### Automation

GitVote allows votes to be created automatically on pull requests when any of the files affected matches certain predefined patterns. For more information about how to set it up, please see the automation section in the [reference documentation](https://github.com/cncf/gitvote/blob/main/docs/config/.gitvote.yml).

> [!IMPORTANT]
> Automatic votes are subject to the same rule as the ones created with the `/vote` command: the user who opens the pull request must be a repository collaborator. When a pull request from any other user matches an automation rule, no vote is created and GitVote posts a comment saying so. In that case, a collaborator can start the vote by calling the `/vote` command.

If the automation rules cannot be evaluated when a pull request is opened (for example, because the configuration file is invalid), or the vote they request cannot be created because of an unexpected failure, no vote is created. GitVote reports a failed check and posts a comment asking maintainers to call the `/vote` command if a vote is needed.

### Voting

Users can cast their votes by reacting to the `git-vote` bot comment where the vote was created (screenshot above).
Expand All @@ -64,7 +71,7 @@ Only votes from users with a binding vote as defined in the configuration file w

### Checking votes

It is possible to check the status of a vote in progress by calling the `/check-vote` command:
It is possible to check the status of a vote in progress by calling the `/check-vote` command (this command does not accept a suffix):

![vote-status](docs/screenshots/vote-status.png)

Expand All @@ -77,18 +84,33 @@ Once the vote time is up, the vote will be automatically closed and the results

![vote-closed](docs/screenshots/vote-closed.png)

If the results cannot be calculated when the vote time is up, for example because GitHub is not available, GitVote keeps trying until it can, waiting up to 6 hours between attempts. The vote is closed once its results are calculated. If publishing them fails (the comment, check, labels or announcement), GitVote retries the parts missing for about 20 hours, and leaves them as they are after that. The results comment and announcement, as well as the cancellation comment, include a hidden marker that identifies the vote, so that a retry finds them instead of posting them again when a previous attempt did post them.

Reactions added after the end of the vote are ignored, even if the vote takes a while to be closed. When the vote is closed early because of the `close_on_passing` feature, its end is the moment it was closed.

> [!NOTE]
> GitHub does not keep track of removed reactions, so removing a reaction after the end of the vote cannot be detected. If that happens before the results are calculated, that reaction will not be counted.

### Cancelling votes

It is possible to cancel a vote in progress by calling the `/cancel-vote` command:
It is possible to cancel a vote in progress by calling the `/cancel-vote` command (this command does not accept a suffix):

![vote-cancelled](docs/screenshots/vote-cancelled.png)

The vote is cancelled as soon as the command is processed, and a new vote can be created right away. If the cancellation cannot be published on GitHub at that moment, GitVote retries publishing it for about 20 hours. When a newer vote exists, only the comment of the cancellation is posted, so that the check and labels of the newer vote are left alone.

If the cancellation still cannot be published after those 20 hours, it is abandoned, and the parts not published yet are left as they are: the cancellation comment may be missing, the `gitvote/open` label may still be on the issue or pull request, and a pull request may keep the in-progress check of the cancelled vote. To clean them up, start a new vote with `/vote` and cancel it with `/cancel-vote` once GitHub is available again, or remove the label by hand. When the check is required and the pull request has no other vote, pushing a new commit also reports a new check on it.

### Checks in pull requests

When a vote on a pull request is closed, GitVote will add a check to the head commit with its result. If the vote passes, the result of the check will be *success*, whereas if it doesn't pass, it'll be *failure*. When used in combination with `protected branch`, this feature can be used to *require* a vote in favor before a pull request can be merged.
When a vote on a pull request is closed, GitVote will add a check with its result to the commit at the head of the pull request at that moment. Commits pushed after the vote is closed do not get that check, even if publishing it has to be retried. The exception is when the head of the pull request cannot be read as the vote closes: the check is then added to the head at the time it is published, which may be a later commit. If the vote passes, the result of the check will be *success*, whereas if it doesn't pass, it'll be *failure*. When used in combination with `protected branch`, this feature can be used to *require* a vote in favor before a pull request can be merged.

The result of the check will also be *failure* if the vote closes without results, which happens when its comment cannot be found. The check will ask to run `/vote` again to start a new vote.

![check-passed](docs/screenshots/check-passed.png)

When the check is required in the target branch and a pull request has no vote, GitVote reports it on each push. The check passes unless an automation rule matches the pull request or the rules cannot be evaluated, in which case it fails. No vote is started on a push, so a vote has to be created with the `/vote` command. This also applies to pull requests where an automatic vote was cancelled.

### Announcements

GitVote is able to post announcements on GitHub discussions. When this feature is enabled, a new discussion will be created with the results of the vote once it is closed.
Expand Down
2 changes: 1 addition & 1 deletion charts/gitvote/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ apiVersion: v2
name: gitvote
description: GitVote is a GitHub application that allows holding a vote on issues and pull requests
type: application
version: 1.5.1-0
version: 1.5.1-1
appVersion: 1.5.0
kubeVersion: ">= 1.19.0-0"
home: https://gitvote.dev
Expand Down
5 changes: 4 additions & 1 deletion charts/gitvote/ci/default-values.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# ct install runs the last published images, so it only checks the chart mechanics
imageTag: latest
gitvote:
github:
# Sample app ID for chart testing
appID: 123456
appID: 1234567
# Sample key for chart testing
appPrivateKey: |-
-----BEGIN RSA PRIVATE KEY-----
Expand Down Expand Up @@ -34,3 +35,5 @@ gitvote:
-----END RSA PRIVATE KEY-----
# Sample webhook secret for chart testing
webhookSecret: "sample-secret-for-chart-testing"
# Sample webhook secret fallback for chart testing
webhookSecretFallback: "sample-fallback-for-chart-testing"
60 changes: 57 additions & 3 deletions charts/gitvote/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,35 @@ app.kubernetes.io/name: {{ include "chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

{{/*
Return the GitHub App ID in decimal, failing on any value that is not a valid App ID.
Unquoted numbers in a values file are read as floats, so whole floats are accepted and
printed in decimal instead of in scientific notation. They must be below 2^53, as floats
cannot hold every whole number past it, so the value read may differ from the one
written: larger IDs must be quoted. Strings are accepted only in decimal without leading
zeros, as helm would read 0123 as 83, and within the int64 range the service uses. An
unquoted 0123 in a values file reaches the chart as 83 already, so it cannot be refused.
Helm versions built before Go 1.18 evaluate every argument of and, so the float checks
are nested to run on floats only.
*/}}
{{- define "chart.githubAppID" -}}
{{- $appID := .Values.gitvote.github.appID -}}
{{- $text := "" -}}
{{- if kindIs "int64" $appID -}}
{{- $text = toString $appID -}}
{{- else if kindIs "float64" $appID -}}
{{- if and (lt $appID 9007199254740992.0) (eq (float64 (int64 $appID)) $appID) -}}
{{- $text = toString (int64 $appID) -}}
{{- end -}}
{{- else if kindIs "string" $appID -}}
{{- $text = $appID -}}
{{- end -}}
{{- if not (and (regexMatch "^[1-9][0-9]*$" $text) (eq (toString (int64 $text)) $text)) -}}
{{- fail "gitvote.github.appID must be the ID of the GitHub App, a positive whole number written without leading zeros" -}}
{{- end -}}
{{- $text -}}
{{- end -}}

{{/*
Kubernetes version
Built-in object .Capabilities.KubeVersion.Minor can provide non-number output
Expand All @@ -75,6 +104,31 @@ longest resource name ("dbmigrator-install" = 18 chars).
{{- end -}}
{{- end -}}

{{/*
Fail on values that must be strings but are not, as helm would print a different value.
Unquoted numbers in a values file are read as floats and printed in scientific notation
from 7 digits, and YAML words like yes or off are read as booleans and printed as true or
false. Other floats are refused too, as the value written cannot be told from them: 0123
and 1.50 reach the chart as 83 and 1.5. Numbers from --set are integers and are printed
unchanged, but helm stores them with the release and reads them back as floats, so they
are refused on upgrades using --reuse-values until set again as strings.
*/}}
{{- define "chart.validateStrings" -}}
{{- $github := .Values.gitvote.github -}}
{{- $values := dict
"db.dbname" .Values.db.dbname
"db.password" .Values.db.password
"db.user" .Values.db.user
"gitvote.github.webhookSecret" $github.webhookSecret
"gitvote.github.webhookSecretFallback" $github.webhookSecretFallback
-}}
{{- range $name, $value := $values -}}
{{- if not (or (kindIs "string" $value) (kindIs "int64" $value) (kindIs "invalid" $value)) -}}
{{- fail (printf "%s must be a string, helm corrupts numbers and booleans: quote it in the values file or pass it with --set-string" $name) -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Provide an init container to verify the database is accessible
*/}}
Expand All @@ -89,11 +143,11 @@ image: {{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag
imagePullPolicy: {{ .Values.pullPolicy }}
env:
- name: PGHOST
value: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host }}
value: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host | quote }}
- name: PGPORT
value: "{{ .Values.db.port }}"
value: {{ .Values.db.port | int | quote }}
- name: PGUSER
value: "{{ .Values.db.user }}"
value: {{ .Values.db.user | quote }}
{{- if $securityContext }}
securityContext:{{- toYaml $securityContext | nindent 2 }}
{{- else }}
Expand Down
1 change: 1 addition & 0 deletions charts/gitvote/templates/dbmigrator_secret.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- include "chart.validateStrings" . -}}
apiVersion: v1
kind: Secret
metadata:
Expand Down
17 changes: 9 additions & 8 deletions charts/gitvote/templates/gitvote_secret.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
{{- include "chart.validateStrings" . -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "chart.resourceNamePrefix" . }}gitvote-config
type: Opaque
stringData:
gitvote.yml: |-
addr: {{ .Values.gitvote.addr }}
addr: {{ .Values.gitvote.addr | quote }}
db:
host: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host }}
port: {{ .Values.db.port | atoi }}
dbname: {{ .Values.db.dbname }}
user: {{ .Values.db.user }}
password: {{ .Values.db.password }}
host: {{ default (printf "%s-postgresql.%s" .Release.Name .Release.Namespace) .Values.db.host | quote }}
port: {{ .Values.db.port | int }}
dbname: {{ .Values.db.dbname | quote }}
user: {{ .Values.db.user | quote }}
password: {{ .Values.db.password | quote }}
log:
format: {{ .Values.log.format }}
format: {{ .Values.log.format | quote }}
github:
appId: {{ .Values.gitvote.github.appID }}
appId: {{ include "chart.githubAppID" . }}
appPrivateKey: {{ .Values.gitvote.github.appPrivateKey | quote }}
webhookSecret: {{ .Values.gitvote.github.webhookSecret | quote }}
{{- with .Values.gitvote.github.webhookSecretFallback }}
Expand Down
2 changes: 1 addition & 1 deletion charts/gitvote/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ gitvote:
github:
# GitHub application ID
appID: null
# GitHub application private key path
# GitHub application private key (PEM content, not a file path)
appPrivateKey: null
# GitHub application webhook secret
webhookSecret: null
Expand Down
2 changes: 1 addition & 1 deletion database/migrations/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Build tern
FROM golang:1.27.3-alpine3.24 AS tern
RUN apk --no-cache add git
RUN go install github.com/jackc/tern@latest
RUN go install github.com/jackc/tern/v2@v2.4.3

# Build final image
FROM alpine:3.24.4
Expand Down
2 changes: 2 additions & 0 deletions database/migrations/functions/001_load_functions.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
-- Loads all the database functions.

{{ template "util/string_to_interval.sql" }}

---- create above / drop below ----
Expand Down
10 changes: 6 additions & 4 deletions database/migrations/functions/util/string_to_interval.sql
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
-- Helper function to validate an interval.
-- Source: https://stackoverflow.com/a/36777132
-- Converts a string to an interval, returning null when it is not a valid interval.
create or replace function string_to_interval(s text)
returns interval as $$
begin
-- Convert the string into an interval
return s::interval;
exception when others then
return null;
exception
-- Report any string that cannot be converted as a missing interval
when others then
return null;
end;
$$ language plpgsql strict;
Loading
Loading