Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various fixes and improvements for the voting tool #1573

Merged
merged 5 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
49 changes: 37 additions & 12 deletions .github/workflows/closeVote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ jobs:
# 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 >>
Expand All @@ -69,20 +68,46 @@ jobs:
- 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"
{
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "markdown<<$EOF"
./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
echo "$EOF"
} >> "$GITHUB_OUTPUT"
env:
COMMENTS: ${{ steps.comments.outputs.comments }}
- name: Install ghcommit
run: go install github.com/planetscale/ghcommit@8c6d9af75a7814768ce871cde246224d45bd8c04
- name: Push to the PR branch
run: git push origin "HEAD:${{ steps.branch.outputs.head }}"
run: |
GH_COMMIT_PATH="$(go env GOPATH)/bin/ghcommit" COMMIT_MESSAGE="$(
git log -1 HEAD --pretty=format:%B
)" SHA="$(
git rev-parse HEAD^
)" DELETED_FILES="$(
git show HEAD --name-only --diff-filter=D --pretty=format:
)" ADDED_FILES="$(
git show HEAD --name-only --diff-filter=d --pretty=format:
)" node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
const {GH_COMMIT_PATH, COMMIT_MESSAGE, SHA, DELETED_FILES, ADDED_FILES} = process.env;
spawnSync(GH_COMMIT_PATH, [
'-r', ${{ toJSON(github.repository) }},
'-b', ${{ toJSON(steps.branch.outputs.head) }},
'-m', COMMIT_MESSAGE,
'--sha', SHA,
...DELETED_FILES.split('\n').filter(Boolean).flatMap(file => ['--delete', file]),
...ADDED_FILES.split('\n').filter(Boolean).flatMap(file => ['--add', file]),
], { stdio: 'inherit' });
EOF
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish vote summary comment
run: |
gh pr comment "${{ steps.pr-url.outputs.URL }}" --body-file summaryComment.md
Expand Down
100 changes: 85 additions & 15 deletions .github/workflows/initiateNewVote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false

jobs:
lint-vote-init-file:
if: github.event.pull_request && github.event.pull_request.draft == false
Expand All @@ -22,8 +26,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
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'
Expand All @@ -40,10 +42,10 @@ jobs:
-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 }}'
-f sha='${{ github.event.pull_request.base.sha }}' || true
gh pr edit ${{ github.event.pull_request.html_url }} --base 'initiateNewVote'
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
initiate-new-vote:
if: github.event.pusher
permissions:
Expand All @@ -54,7 +56,7 @@ jobs:
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
persist-credentials: true # we need the credentials to push the new vote branch
fetch-depth: 2
- name: Extract info from the pushed file
id: data
run: |
Expand Down Expand Up @@ -105,9 +107,9 @@ jobs:
gpg-agent --daemon --allow-preset-passphrase \
--default-cache-ttl 60 --max-cache-ttl 60
fi
- name: Generate the vote branch and PR
- name: Generate the vote init commit
run: |
./votes/initiateNewVote/generateNewVotePR.mjs \
./votes/initiateNewVote/generateNewVote.mjs \
--remote origin \
--github-repo-name "$GITHUB_REPOSITORY" \
--vote-repository-path . \
Expand All @@ -116,22 +118,90 @@ jobs:
${{ env.__CANDIDATES }} \
--shuffle-candidates "$__SHUFFLE_CANDIDATES" \
--header-instructions "$__HEADER_INSTRUCTIONS" \
--footer-instructions "$__FOOTER_INSTRUCTIONS" \
--create-pull-request --pr-intro "$__PR_INTRO"
--footer-instructions "$__FOOTER_INSTRUCTIONS"
env:
GH_TOKEN: ${{ github.token }}
GITHUB_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
- name: Install ghcommit
run: go install github.com/planetscale/ghcommit@8c6d9af75a7814768ce871cde246224d45bd8c04
- run: git reset HEAD --hard
- name: Generate the vote branch
run: |
gh api \
--method DELETE \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$GITHUB_REPOSITORY/git/$GITHUB_REF"
/repos/${{ github.repository }}/git/refs \
-f "ref=refs/heads/${{ steps.data.outputs.branchName }}" -f "sha=$(git rev-parse HEAD^)"

GH_COMMIT_PATH="$(go env GOPATH)/bin/ghcommit" COMMIT_MESSAGE="$(
git log -1 HEAD --pretty=format:%B
)" SHA="$(
git rev-parse HEAD^
)" DELETED_FILES="$(
git show HEAD --name-only --diff-filter=D --pretty=format:
)" ADDED_FILES="$(
git show HEAD --name-only --diff-filter=d --pretty=format:
)" node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
const {GH_COMMIT_PATH, COMMIT_MESSAGE, SHA, DELETED_FILES, ADDED_FILES} = process.env;
spawnSync(GH_COMMIT_PATH, [
'-r', ${{ toJSON(github.repository) }},
'-b', ${{ toJSON(steps.data.outputs.branchName) }},
'-m', COMMIT_MESSAGE,
'--sha', SHA,
...DELETED_FILES.split('\n').filter(Boolean).flatMap(file => ['--delete', file]),
...ADDED_FILES.split('\n').filter(Boolean).flatMap(file => ['--add', file]),
], { stdio: 'inherit' });
EOF
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Open the vote PR
run: |
./votes/initiateNewVote/generateNewVotePR.mjs \
--github-repo-name "$GITHUB_REPOSITORY" \
--branch "$__BRANCH" \
--subject "$__SUBJECT" \
--pr-intro "$__PR_INTRO"
env:
GITHUB_TOKEN: ${{ github.token }}
__BRANCH: ${{ steps.data.outputs.branchName }}
__SUBJECT: ${{ fromJSON(steps.data.outputs.json_data).subject }}
__PR_INTRO: ${{ fromJSON(steps.data.outputs.json_data).prBody }}
- name: Remove initiateNewVote branch if there are no open PRs, or revert commit
run: |
set -x
if [[ "$(gh search prs --repo=${{ github.repository }} --state open -B initiateNewVote --jq '. | length' --json id)" == "0" ]]; then
gh api \
--method DELETE \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"/repos/$GITHUB_REPOSITORY/git/$GITHUB_REF"
else
git reset ${{ github.sha }} --hard
git revert HEAD --no-edit
GH_COMMIT_PATH="$(go env GOPATH)/bin/ghcommit" COMMIT_MESSAGE="$(
git log -1 HEAD --pretty=format:%B
)" DELETED_FILES="$(
git show HEAD --name-only --diff-filter=D --pretty=format:
)" ADDED_FILES="$(
git show HEAD --name-only --diff-filter=d --pretty=format:
)" node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
const {GH_COMMIT_PATH, COMMIT_MESSAGE, DELETED_FILES, ADDED_FILES} = process.env;
spawnSync(GH_COMMIT_PATH, [
'-r', ${{ toJSON(github.repository) }},
'-b', 'initiateNewVote',
'-m', COMMIT_MESSAGE,
'--sha', ${{ toJSON(github.sha) }},
...DELETED_FILES.split('\n').filter(Boolean).flatMap(file => ['--delete', file]),
...ADDED_FILES.split('\n').filter(Boolean).flatMap(file => ['--add', file]),
], { stdio: 'inherit' });
EOF
fi
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
103 changes: 103 additions & 0 deletions votes/initiateNewVote/_generateNewVotePR.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { once } from "node:events";
import { spawn } from "node:child_process";
import { exit } from "node:process";
import { Buffer } from "node:buffer";

export const shareholdersThreshold = 3;

export const keyServerURL = "hkps://keys.openpgp.org";


export const prOptions = {
["github-repo-name"]: {
type: "string",
describe: "GitHub repository, in the format owner/repo",
default: "nodejs/TSC",
},
"pr-intro": {
type: "string",
describe: "Add an intro in markdown format for the PR body",
},
branch: {
type: "string",
short: "b",
describe: "Name of the branch and subdirectory to use for the tests",
demandOption: true,
},
subject: {
type: "string",
short: "s",
},
}

export async function createVotePR(argv) {
const cp = spawn(
"gh",
[
"api",
`repos/${argv["github-repo-name"]}/pulls`,
"-F",
"base=main",
"-F",
`head=${argv.branch}`,
"-F",
`title=${argv.subject}`,
"-F",
`body=${argv["pr-intro"] ?? ""},

/cc @nodejs/tsc

To close the vote, a minimum of ${shareholdersThreshold} key parts would need to be revealed.

Vote instructions will follow.`,
"--jq",
".html_url",
],
{ stdio: ["inherit", "pipe", "inherit"] }
);
// @ts-ignore toArray does exist!
const out = cp.stdout.toArray();
const [code] = await once(cp, "exit");
if (code !== 0) exit(code);

const prUrl = Buffer.concat(await out)
.toString()
.trim();

{
const cp = spawn(
"gh",
[
"pr",
"edit",
prUrl,
"--body",
`${argv["pr-intro"] ?? ""}

Vote instructions:

- on the web: <https://nodejs.github.io/caritat/#${prUrl}>
- on the CLI:
${"```sh"}
git node vote ${prUrl}
${"`"}
aduh95 marked this conversation as resolved.
Show resolved Hide resolved

To close the vote, at least ${shareholdersThreshold} secret holder(s)[^1] must \
run the following command: ${"`"}git node vote ${prUrl} --decrypt-key-part --post-comment${"`"}

/cc @nodejs/tsc

[^1]: secret holders are folks who have access to the private key associated with \
a public key on <${keyServerURL}> that references an email address listed on the \
TSC voting member list at the time of the opening of the vote.
`,
],
{ stdio: "inherit" },
);

const [code] = await once(cp, "exit");
if (code !== 0) exit(code);
}

console.log("PR created", prUrl);
}
5 changes: 3 additions & 2 deletions votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ const { values: parsedArgs } = parseArgs({
},
});

const keyParts = JSON.parse(parsedArgs.comments)
const keyParts = [...new Set(JSON.parse(parsedArgs.comments)
.map(
(txt) =>
/-----BEGIN SHAMIR KEY PART-----(.+)-----END SHAMIR KEY PART-----/s.exec(
txt
)?.[1]
)
.filter(Boolean);
.filter(Boolean))];

const firstCommitRef = parsedArgs.fromCommit;
const voteFileCanonicalName = "vote.yml";
Expand Down Expand Up @@ -86,6 +86,7 @@ const { result, privateKey } = await countFromGit({
firstCommitRef,
lastCommitRef: parsedArgs.toCommit,
mailmap: parsedArgs.mailmap,
pushToRemote: false,
commitJsonSummary: parsedArgs["commit-json-summary"]
? {
refs: parsedArgs.prURL,
Expand Down
Loading