Skip to content

Commit

Permalink
Add vote lifecycle workflows and scripts (#1401) (#1522)
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 authored Apr 10, 2024
1 parent 3576107 commit 2bfba10
Show file tree
Hide file tree
Showing 9 changed files with 840 additions and 0 deletions.
91 changes: 91 additions & 0 deletions .github/workflows/closeVote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Close vote

on:
# Using `issue_comment` is a bit noisy, let's disable it for now.
# issue_comment:
# types: [created]
workflow_dispatch:
inputs:
pr:
description: ID of the Vote PR that contains a vote ready to be closed
required: true
type: number

permissions:
contents: write
pull-requests: write

jobs:
close-vote:
if: github.event.inputs.pr ||
(github.event.issue.pull_request && contains(github.event.comment.body, '-----BEGIN SHAMIR KEY PART-----'))
runs-on: ubuntu-latest
steps:
- name: Get PR URL
id: pr-url
run: |
echo "URL=${{ github.event.repository.html_url }}/pull/${{ github.event.inputs.pr || github.event.issue.number }}" >> "$GITHUB_OUTPUT"
- name: Filter comments
id: comments
run: gh pr view ${{ steps.pr-url.outputs.URL }} --json
comments --jq '.comments | map(.body | select(contains("-----BEGIN
SHAMIR KEY PART-----"))) | "comments=" + tostring' >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Get PR branch
id: branch
run: gh pr view ${{ steps.pr-url.outputs.URL }} --json
headRefName --jq '"head=" + .headRefName' >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Compute number of commits in the PR
id: nb-of-commits
run: |
NB_OF_COMMITS=$(gh pr view --json commits --jq '.commits | length' "${{ steps.pr-url.outputs.URL }}")
echo "exact=$NB_OF_COMMITS" >> $GITHUB_OUTPUT
echo "minusOne=$(($NB_OF_COMMITS - 1))" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ github.token }}
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
# Loading the default branch so we use the last version of the mailmap
# rather than getting stuck to when the vote PR was open.
ref: ${{ github.event.repository.default_branch }}
persist-credentials: true # we need the credentials to push the new vote branch
- name: Download nodejs/node mailmap file
run:
curl -L https://raw.githubusercontent.com/nodejs/node/main/.mailmap >>
.mailmap
- name: Configure git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Node.js GitHub Bot"
- name: Load vote branch
run: |
git fetch origin '${{ steps.branch.outputs.head }}'
git reset FETCH_HEAD --mixed
git checkout HEAD -- '${{ steps.branch.outputs.head }}'
- run: npm install @node-core/caritat
- name: Attempt closing the vote
id: vote-summary
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "markdown<<$EOF" >> "$GITHUB_OUTPUT"
./votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs \
--remote origin --branch "${{ steps.branch.outputs.head }}" \
--fromCommit "FETCH_HEAD~${{ steps.nb-of-commits.outputs.minusOne }}" \
--toCommit "FETCH_HEAD" \
--prURL "${{ steps.pr-url.outputs.URL }}" \
--save-markdown-summary summaryComment.md \
--comments "$COMMENTS" --commit-json-summary >> "$GITHUB_OUTPUT"
echo "$EOF" >> "$GITHUB_OUTPUT"
env:
COMMENTS: ${{ steps.comments.outputs.comments }}
- name: Push to the PR branch
run: git push origin "HEAD:${{ steps.branch.outputs.head }}"
- name: Publish vote summary comment
run: |
gh pr comment "${{ steps.pr-url.outputs.URL }}" --body-file summaryComment.md
env:
GH_TOKEN: ${{ github.token }}
SUMMARY: ${{ steps.vote-summary.outputs.markdown }}
145 changes: 145 additions & 0 deletions .github/workflows/initiateNewVote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: Initiate new vote

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- votes/initiateNewVote/_EDIT_ME.yml
push:
branches:
- initiateNewVote

permissions:
contents: read

jobs:
lint-vote-init-file:
if: github.event.pull_request && github.event.pull_request.draft == false
permissions:
contents: write
pull-requests: write
repository-projects: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
persist-credentials: false
# If the subject is still REPLACEME, that would mean it's a PR to modify
# the sample file, not a PR initializing a vote.
- run: '! grep -q "subject: REPLACEME" votes/initiateNewVote.yml'
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with:
node-version: lts/*
- name: Validate YAML and ensure there are more than 1 candidate
run:
npx js-yaml votes/initiateNewVote.yml | jq '.candidates | unique |
length > 1 or error("Not enough candidates")'
- name: Change base branch
if: github.base_ref == github.event.repository.default_branch
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/git/refs \
-f ref='refs/heads/initiateNewVote' \
-f sha='${{ github.event.pull_request.base.sha }}'
gh pr edit ${{ github.event.pull_request.html_url }} --base 'initiateNewVote'
env:
GH_TOKEN: ${{ github.token }}
initiate-new-vote:
if: github.event.pusher
permissions:
contents: write
pull-requests: write
repository-projects: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
persist-credentials: true # we need the credentials to push the new vote branch
- name: Install Node.js
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0
with:
node-version: lts/*
- name: Extract info from the pushed file
id: data
run: |
npx js-yaml votes/initiateNewVote/_EDIT_ME.yml > data.json
echo "json_data<<EOF" >> "$GITHUB_OUTPUT"
cat data.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "branchName=votes/$(node -p 'require("./data.json")["path-friendly-id"] || crypto.randomUUID()')" >> "$GITHUB_OUTPUT"
node >> "$GITHUB_ENV" <<'EOF'
'use strict';
const { createHash } = require('node:crypto');
const { candidates } = require("./data.json");
for (let i = 0; i < candidates.length; i++) {
const delimiter = createHash('sha256').update(candidates[i], 'utf8').digest('base64');
console.log(`__CANDIDATES_${i}<<${delimiter}`)
process.stdout.write(candidates[i]);
process.stdout.write(`\n${delimiter}\n`);
}
console.log('__CANDIDATES<<EOF');
for (let i = 0; i < candidates.length; i++) {
console.log(`--candidate "$__CANDIDATES_${i}" \\`);
}
if (candidates.length) console.log('');
console.log('EOF');
EOF
- name: Reset to the base branch
run: git fetch origin HEAD && git reset FETCH_HEAD --hard
- name: Install npm dependencies
run: npm install @node-core/caritat
- name: Configure git
run: |
git config --global user.email "[email protected]"
git config --global user.name "Node.js GitHub Bot"
- name: Configure and (re)start GPG agent
shell: bash
run: |
if [ -f /usr/lib/systemd/user/gpg-agent.service ]; then
mkdir ~/.gnupg
cat <<EOT >> ~/.gnupg/gpg-agent.conf
allow-preset-passphrase
default-cache-ttl 60
max-cache-ttl 50
EOT
chmod 600 ~/.gnupg/*
chmod 700 ~/.gnupg
systemctl --user restart gpg-agent
else
gpg-agent --daemon --allow-preset-passphrase \
--default-cache-ttl 60 --max-cache-ttl 60
fi
- name: Generate the vote branch and PR
run: |
./votes/initiateNewVote/generateNewVotePR.mjs \
--remote origin \
--github-repo-name "$GITHUB_REPOSITORY" \
--vote-repository-path . \
--branch "$__BRANCH" \
--subject "$__SUBJECT" \
${{ env.__CANDIDATES }} \
--shuffle-candidates "$__SHUFFLE_CANDIDATES" \
--header-instructions "$__HEADER_INSTRUCTIONS" \
--footer-instructions "$__FOOTER_INSTRUCTIONS" \
--create-pull-request --pr-intro "$__PR_INTRO"
env:
GH_TOKEN: ${{ github.token }}
__BRANCH: ${{ steps.data.outputs.branchName }}
__SUBJECT: ${{ fromJSON(steps.data.outputs.json_data).subject }}
__SHUFFLE_CANDIDATES: ${{ fromJSON(steps.data.outputs.json_data).canShuffleCandidates }}
__HEADER_INSTRUCTIONS: ${{ fromJSON(steps.data.outputs.json_data).headerInstructions }}
__FOOTER_INSTRUCTIONS: ${{ fromJSON(steps.data.outputs.json_data).footerInstructions }}
__PR_INTRO: ${{ fromJSON(steps.data.outputs.json_data).prBody }}
- name: Remove initiateNewVote branch
run: |
gh api \
--method DELETE \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$GITHUB_REPOSITORY/git/$GITHUB_REF"
env:
GH_TOKEN: ${{ github.token }}
70 changes: 70 additions & 0 deletions .github/workflows/watchVote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Validate vote commit and update participation

on:
pull_request:
types: [synchronize]
paths: [votes/**]

concurrency: ${{ github.workflow }}--${{ github.head_ref }}
permissions:
contents: read
pull-requests: write
repository-projects: read

jobs:
validate-commit-and-update-participation:
if: startsWith(github.head_ref, 'votes/')
runs-on: ubuntu-latest
steps:
- name: Compute number of commits in the PR
id: nb-of-commits
run: |
echo "plusOne=$((${{ github.event.pull_request.commits }} + 1))" >> $GITHUB_OUTPUT
echo "minusOne=$((${{ github.event.pull_request.commits }} - 1))" >> $GITHUB_OUTPUT
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
fetch-depth: ${{ steps.nb-of-commits.outputs.plusOne }}
persist-credentials: false
- name: Download nodejs/node mailmap file
run:
curl -L https://raw.githubusercontent.com/nodejs/node/main/.mailmap >>
.mailmap
- run: npm install @node-core/caritat
- name: Get PR description
id: desc
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "markdown<<$EOF" >> "$GITHUB_OUTPUT"
gh pr view "${{ github.event.pull_request.html_url }}" --json body --jq '.body' >> "$GITHUB_OUTPUT"
echo "$EOF" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: ${{ github.token }}
- name: Get updated vote status
id: status
run:
echo "prStatus=$(./votes/initiateNewVote/getVoteStatus.mjs)" >>
"$GITHUB_OUTPUT"; cat "$GITHUB_OUTPUT"
env:
SUBPATH: ${{ github.head_ref }}
FIRST_COMMIT_REF: HEAD^2~${{ steps.nb-of-commits.outputs.minusOne }}
LAST_COMMIT_REF: ${{ github.event.after }}
CHECK_COMMITS_AFTER: ${{ github.event.before }}
PR_DESCRIPTION: ${{steps.desc.outputs.markdown}}
- name: Update PR description
run:
gh pr edit "${{ github.event.pull_request.html_url }}" --body "$BODY"
env:
BODY: ${{ fromJSON(steps.status.outputs.prStatus).body }}
GH_TOKEN: ${{ github.token }}
- name: Add comment if some invalid commits were found
if: fromJSON(steps.status.outputs.prStatus).hasFailures
run:
gh pr comment "${{ github.event.pull_request.html_url }}" -b
"$SUMMARY"
env:
GH_TOKEN: ${{ github.token }}
SUMMARY:
${{ fromJSON(steps.status.outputs.prStatus).invalidCommitReason }}
- name: Mark workflow as failed if some invalid commits were found
if: fromJSON(steps.status.outputs.prStatus).hasFailures
run: 'false'
25 changes: 25 additions & 0 deletions votes/initiateNewVote/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Initiate a new TSC vote

For yes/no questions, the TSC will typically use GH reactions to conduct a vote,
and use this workflow only for questions with several candidate answers, or to
e.g guarantee vote secrecy until the vote is counted.

## From the GitHub web UI

1. Edit the [`_EDIT_ME.yml`](./_EDIT_ME.yml) file, fill in the info related to
vote to open.
2. When committing, chose to commit to new branch and open a Pull Request to
discuss the vote terms with the whole TSC.
3. Once the PR has approvals, merge it on the `initiateNewVote` branch (GHA
should have set that as the target/base branch automatically).
4. GHA will open a new PR with the vote initiated.

## From the CLI

This method is not recommended.

1. Edit the [`_EDIT_ME.yml`](./_EDIT_ME.yml) file, fill in the info related to
vote to open.
2. Commit your changes.
3. Push that to the remote `refs/heads/initiateNewVote`.
4. GHA will open a new PR with the vote initiated.
45 changes: 45 additions & 0 deletions votes/initiateNewVote/_EDIT_ME.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# To initiate a new vote, you need to open a PR modifying this file. The vote
# can start once the PR has approvals and is merged via the GitHub interface.

# 1. Select a subject for the vote. This can be a question addressed to the TSC
# voting members.
subject: REPLACEME

# 2. You can leave the header instructions as is, or modify them if you see fit.
headerInstructions: |
Please set a score to each proposal according to your preferences.
You should set the highest score to your favorite option.
Negative scores are allowed, only the order matters.
You can tie two or more proposals if you have no preference.
To abstain, keep all the propositions tied.
# 3. Give a list of "candidates". Those should be answers to the subject
# question, and should leave as little room to interpretation as possible. Do
# not list candidates that don't have a champion, there should be a
# clear plan for each candidates in the event where it wins the vote; listing
# a "troll candidate" will only hurt the credibility of the voting process if
# it wins and everyone realize we have to re-take the vote because it can't
# happen. Don't hesitate to list very similar candidates, with however small
# nuances: we are using the Condorcet method to count the votes, which lets
# voters express their preference for each candidates, no matter how many
# there are.
candidates:
- TODO
- TODO

# 4. Pass the following to false if it's important to keep the candidates in the
# order you define above. Presenting candidates in a fixed order tends to
# give an unfair advantage to the first option.
canShuffleCandidates: true

# 5. Insert here a short description of the vote objectives and link to the
# issue it was discussed on to give the full context.
footerInstructions: |
TBD
# 6. Optionally, insert a brief introduction for the vote PR, in the markdown format.
prBody: |
# 7. Optionally, choose an id that will be used for the branch name as well as
# the vote folder name. If not supplied, a UUID will be used.
path-friendly-id: null
Loading

0 comments on commit 2bfba10

Please sign in to comment.