Skip to content

Commit

Permalink
Various fixes and improvements for the voting tool (#1573)
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 committed Jun 14, 2024
1 parent 460889c commit f523773
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 298 deletions.
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}
${"```"}
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

0 comments on commit f523773

Please sign in to comment.