From 3da0cbaf447752fcbf4789dc9fbb35c921997b48 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sun, 11 Jun 2023 21:49:54 +0200 Subject: [PATCH 1/9] Add vote lifecycle workflows --- .github/workflows/closeVote.yml | 92 ++++++ .github/workflows/initiateNewVote.yml | 122 ++++++++ .github/workflows/watchVote.yml | 70 +++++ votes/initiateNewVote/README.md | 25 ++ votes/initiateNewVote/_EDIT_ME.yml | 45 +++ .../decryptPrivateKeyAndCloseVote.mjs | 113 ++++++++ .../initiateNewVote/extractInfoFromReadme.mjs | 19 ++ votes/initiateNewVote/generateNewVotePR.mjs | 272 ++++++++++++++++++ votes/initiateNewVote/getVoteStatus.mjs | 58 ++++ 9 files changed, 816 insertions(+) create mode 100644 .github/workflows/closeVote.yml create mode 100644 .github/workflows/initiateNewVote.yml create mode 100644 .github/workflows/watchVote.yml create mode 100644 votes/initiateNewVote/README.md create mode 100644 votes/initiateNewVote/_EDIT_ME.yml create mode 100755 votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs create mode 100644 votes/initiateNewVote/extractInfoFromReadme.mjs create mode 100755 votes/initiateNewVote/generateNewVotePR.mjs create mode 100755 votes/initiateNewVote/getVoteStatus.mjs diff --git a/.github/workflows/closeVote.yml b/.github/workflows/closeVote.yml new file mode 100644 index 00000000..0851e8c4 --- /dev/null +++ b/.github/workflows/closeVote.yml @@ -0,0 +1,92 @@ +name: Close vote + +on: + 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 "github-bot@iojs.org" + 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 @aduh95/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 }} diff --git a/.github/workflows/initiateNewVote.yml b/.github/workflows/initiateNewVote.yml new file mode 100644 index 00000000..5bf2ada9 --- /dev/null +++ b/.github/workflows/initiateNewVote.yml @@ -0,0 +1,122 @@ +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<> "$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" + - name: Reset to the base branch + run: git fetch origin HEAD && git reset FETCH_HEAD --hard + - name: Install npm dependencies + run: npm install @aduh95/caritat + - name: Configure git + run: | + git config --global user.email "github-bot@iojs.org" + 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 <> ~/.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 + run: | + ./votes/initiateNewVote/generateNewVotePR.mjs \ + --remote origin \ + --github-repo-name '${{ github.repository }}' \ + --tsc-repository-path . \ + --branch '${{ steps.data.outputs.branchName }}' \ + --subject '${{ fromJSON(steps.data.outputs.json_data).subject }}' \ + --candidate '${{ join(fromJSON(steps.data.outputs.json_data).candidates, ''' --candidate ''') }}' \ + --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 }}' \ + --create-pull-request --pr-intro '${{ fromJSON(steps.data.outputs.json_data).prBody }}' + env: + GH_TOKEN: ${{ github.token }} + - 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 }} diff --git a/.github/workflows/watchVote.yml b/.github/workflows/watchVote.yml new file mode 100644 index 00000000..d23ce274 --- /dev/null +++ b/.github/workflows/watchVote.yml @@ -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 @aduh95/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" diff --git a/votes/initiateNewVote/README.md b/votes/initiateNewVote/README.md new file mode 100644 index 00000000..0604ff55 --- /dev/null +++ b/votes/initiateNewVote/README.md @@ -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. diff --git a/votes/initiateNewVote/_EDIT_ME.yml b/votes/initiateNewVote/_EDIT_ME.yml new file mode 100644 index 00000000..d0d5ebe1 --- /dev/null +++ b/votes/initiateNewVote/_EDIT_ME.yml @@ -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 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 tied 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 diff --git a/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs b/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs new file mode 100755 index 00000000..daf8527d --- /dev/null +++ b/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import { writeFile } from "node:fs/promises"; +import { createInterface as readLines } from "node:readline"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; + +import countFromGit from "@aduh95/caritat/countBallotsFromGit"; + +const { values: parsedArgs } = parseArgs({ + options: { + remote: { + type: "string", + describe: + "Name or URL to the remote repo. If not provided, SSH pointing to --github-repo-name will be used.", + }, + branch: { + type: "string", + describe: "Name of the branch where the vote takes place", + }, + mailmap: { + type: "string", + describe: "Path to the mailmap file (if any)", + }, + fromCommit: { + describe: "reference to the commit initiating the vote", + type: "string", + }, + toCommit: { + type: "string", + describe: "reference to the last vote commit", + }, + prURL: { + describe: "URL to the PR to add to the vote summary file", + type: "string", + }, + comments: { + describe: "The body of the PR comments that contains the key part(s)", + type: "string", + }, + "commit-json-summary": { + describe: "Commit JSON summary", + type: "boolean", + }, + "save-markdown-summary": { + describe: "Write the markdown to a file (use - for stdout)", + type: "string", + }, + }, +}); + +const keyParts = JSON.parse(parsedArgs.comments) + .map( + (txt) => + /-----BEGIN SHAMIR KEY PART-----(.+)-----END SHAMIR KEY PART-----/s.exec( + txt + )?.[1] + ) + .filter(Boolean); + +const firstCommitRef = parsedArgs.fromCommit; +const voteFileCanonicalName = "vote.yml"; + +const subPath = await new Promise(async (resolve, reject) => { + const cp = spawn("git", [ + "--no-pager", + "show", + firstCommitRef, + "--name-only", + ]); + cp.on("error", reject); + for await (const line of readLines(cp.stdout)) { + if (line === voteFileCanonicalName) return resolve("./"); + if (line.endsWith(`/${voteFileCanonicalName}`)) + return resolve(line.slice(0, -voteFileCanonicalName.length)); + } +}); + +const { result, privateKey } = await countFromGit({ + cwd: fileURLToPath(new URL("../../", import.meta.url)), + repoURL: parsedArgs.remote, + branch: parsedArgs.branch, + subPath, + keyParts, + firstCommitRef, + lastCommitRef: parsedArgs.toCommit, + mailmap: parsedArgs.mailmap, + commitJsonSummary: parsedArgs["commit-json-summary"] + ? { + refs: parsedArgs.prURL, + } + : null, +}); + +if (parsedArgs["save-markdown-summary"]) { + function* toArmoredMessage(str, chunkSize = 64) { + yield "-----BEGIN PRIVATE KEY-----"; + for (let i = 0; i < str.length; i += chunkSize) { + yield str.substr(i, chunkSize); + } + yield "-----END PRIVATE KEY-----"; + } + await writeFile( + parsedArgs["save-markdown-summary"], + result.generateSummary( + Array.from( + toArmoredMessage(Buffer.from(privateKey).toString("base64")) + ).join("\n") + ), + "utf8" + ); +} diff --git a/votes/initiateNewVote/extractInfoFromReadme.mjs b/votes/initiateNewVote/extractInfoFromReadme.mjs new file mode 100644 index 00000000..10b9c04d --- /dev/null +++ b/votes/initiateNewVote/extractInfoFromReadme.mjs @@ -0,0 +1,19 @@ +export default async function* exportInfoFromReadme(iterator) { + const handleLine = /^\* \[([^\]]+)\]\(/; + const nameAndEmailLine = /^\s\s\*\*([^*]+)\*\* <<([^>]+)>>(?: \([^)]+\))?$/; + let isInsideTSCSection = false; + let currentMemberHandle; + for await (const line of iterator) { + if (currentMemberHandle != null) { + const [, name, email] = nameAndEmailLine.exec(line); + yield { handle: currentMemberHandle, name, email }; + currentMemberHandle = null; + } else if (isInsideTSCSection && line === "#### TSC regular members") { + break; + } else if (isInsideTSCSection && line.charAt(0) === "*") { + currentMemberHandle = handleLine.exec(line)[1]; + } else if (line === "#### TSC voting members") { + isInsideTSCSection = true; + } + } +} diff --git a/votes/initiateNewVote/generateNewVotePR.mjs b/votes/initiateNewVote/generateNewVotePR.mjs new file mode 100755 index 00000000..f88064bc --- /dev/null +++ b/votes/initiateNewVote/generateNewVotePR.mjs @@ -0,0 +1,272 @@ +#!/usr/bin/env node + +import { createInterface as readLines } from "node:readline"; +import { exit } from "node:process"; +import { get } from "node:https"; +import { resolve } from "node:path"; +import { once } from "node:events"; +import { open, readFile } from "node:fs/promises"; +import { parseArgs } from "node:util"; +import { spawn } from "node:child_process"; + +import generateNewVoteFolder from "@aduh95/caritat/generateNewVoteFolder"; + +import readReadme from "./extractInfoFromReadme.mjs"; + +const { values: argv } = parseArgs({ + options: { + remote: { + type: "string", + describe: + "Name or URL to the remote repo. If not provided, SSH pointing to --github-repo-name will be used.", + }, + ["github-repo-name"]: { + type: "string", + describe: "GitHub repository, in the format owner/repo", + default: "nodejs/TSC", + }, + "create-pull-request": { + type: "boolean", + describe: "Use GitHub API to create a Pull Request. Requires gh CLI tool", + }, + "pr-intro": { + type: "string", + describe: "Add an intro in markdown format for the PR body", + }, + "gpg-binary": { + type: "string", + }, + ["gpg-sign"]: { + type: "boolean", + short: "S", + describe: "GPG-sign commits.", + }, + "nodejs-repository-path": { + type: "string", + short: "r", + }, + "tsc-repository-path": { + type: "string", + short: "R", + description: + "Path to the local nodejs/TSC repository. If not provided, it will be fetched from GitHub", + }, + subject: { + type: "string", + short: "s", + }, + branch: { + type: "string", + short: "b", + describe: "Name of the branch and subdirectory to use for the tests", + demandOption: true, + }, + candidate: { + type: "string", + multiple: true, + short: "c", + }, + "shuffle-candidates": { + type: "string", + }, + "header-instructions": { + type: "string", + }, + "footer-instructions": { + type: "string", + }, + vote: { + type: "boolean", + describe: "Register a vote just after the vote is initialized", + }, + abstain: { + type: "boolean", + describe: + "Use this flag to create a blank ballot and skip the voting if --vote is provided", + }, + "do-not-clean": { + type: "boolean", + describe: "Use this flag to keep temp files on the local FS", + }, + help: { + type: "boolean", + short: "h", + }, + version: { + type: "boolean", + short: "v", + }, + }, +}); + +if (argv.help) { + // TODO parse args subjects + console.log("Mandatory flags:"); + console.log( + "\t--branch (alias -b): A name for the vote to take place. It will also be used for naming the subfolder." + ); + console.log("\t--subject (alias -s): Subject of vote."); + console.log("Node.js specific options:"); + console.log( + "\t--nodejs-repository-path (alias -r): Path to a local clone of " + + "nodejs/node. If not provided, files will be downloaded from HTTPS." + ); + console.log( + "\t--tsc-repository-path (alias -R): Path to a local clone of " + + "nodejs/TSC. If not provided, it will be cloned from SSH (or HTTPS if " + + "an HTTPS remote is provided)." + ); + console.log( + "\t--github-repo-name: GitHub repository, in the format owner/repo. Default is nodejs/TSC." + ); + console.log( + "\t--remote: Default is git@github.com:${{--github-repo-name}}.git." + ); + exit(0); +} + +if (argv.version) { + const pJson = await readFile( + new URL("../../package.json", import.meta.url), + "utf-8" + ); + console.log(JSON.parse(pJson).version); + exit(0); +} + +if (!argv.branch) { + throw new Error("You must pass a branch name"); +} +if (!argv.subject) { + throw new Error("You must pass a subject"); +} + +let input, crlfDelay; +if (argv["nodejs-repository-path"] == null) { + input = await fetch( + "https://raw.githubusercontent.com/nodejs/node/main/README.md" + ).then((res) => { + if (!res.ok) { + throw new Error("Wrong status code", { cause: res }); + } else { + return res.text(); + } + }); +} else { + const fh = await open( + join(resolve(argv["nodejs-repository-path"]), "README.md"), + "r" + ); + input = fh.createReadStream(); + crlfDelay = Infinity; +} + +const tscMembersArray = []; +for await (const member of readReadme(readLines({ input, crlfDelay }))) { + tscMembersArray.push(member); +} + +input.destroy?.(); + +const shareholderThreshold = 2; // Math.ceil(tscMembersArray.length / 4); + +const keyServerURL = "hkps://keys.openpgp.org"; + +await generateNewVoteFolder({ + candidates: argv.candidate, + headerInstructions: argv["header-instructions"], + footerInstructions: argv["footer-instructions"], + canShuffleCandidates: argv["shuffle-candidates"] !== "false", + subject: argv.subject, + gpgOptions: { + binaryPath: argv["gpg-binary"], + trustModel: "always", + keyServerURL, + }, + gitOptions: { + repo: argv.remote ?? `git@github.com:${argv["github-repo-name"]}.git`, + branch: argv.branch, + baseBranch: "main", + forceClone: !argv["tsc-repository-path"], + }, + shareholdersThreshold: shareholderThreshold, + shareholders: tscMembersArray.map(({ email }) => email), + allowedVoters: tscMembersArray.map( + (voter) => `${voter.name} <${voter.email}>` + ), + method: "Condorcet", + path: argv["tsc-repository-path"] + ? resolve(argv["tsc-repository-path"], argv.branch) + : argv.branch, +}); + +if (argv["create-pull-request"]) { + 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"] ?? ""} + +The following users are invited to participate in this vote: + +${tscMembersArray + .map(({ name, handle }) => `- ${name} @${handle} (TSC)`) + .join("\n")} + +To close the vote, a minimum of ${shareholderThreshold} 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 CLI: ${"`"}git node vote ${prUrl}${"`"} +- on the web: + +To close the vote, at least ${shareholderThreshold} secret holder(s)[^1] must \ +run the following command: ${"`"}git node vote ${prUrl} --decrypt-key-part --post-comment${"`"} + +[^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); +} diff --git a/votes/initiateNewVote/getVoteStatus.mjs b/votes/initiateNewVote/getVoteStatus.mjs new file mode 100755 index 00000000..99d02705 --- /dev/null +++ b/votes/initiateNewVote/getVoteStatus.mjs @@ -0,0 +1,58 @@ +#!/usr/bin/env node + +import { exit } from "node:process"; +import { env } from "node:process"; +import count from "@aduh95/caritat/countParticipationFromGit"; + +const START_MARKER = ""; +const END_MARKER = ""; + +const participationResult = await count({ + subPath: env.SUBPATH, + firstCommitRef: env.FIRST_COMMIT_REF, + lastCommitRef: env.LAST_COMMIT_REF, + reportInvalidCommitsAfter: env.CHECK_COMMITS_AFTER, +}); + +const participation = participationResult?.participation; + +if (participation == null) { + console.error("Can't compute participation", participationResult); + exit(1); +} + +const mdParticipation = `\n\n${START_MARKER}\n\nCurrent estimated participation: ${ + Math.round(participation * 100_00) / 100 +}%\n\n${END_MARKER}\n`; + +let body = env.PR_DESCRIPTION || ""; +const startMarkerIndex = body.indexOf(START_MARKER); + +if (startMarkerIndex === -1) { + body += mdParticipation; +} else { + const endMarkerIndex = body.lastIndexOf(END_MARKER) + END_MARKER.length; + body = + body.slice(0, startMarkerIndex) + + mdParticipation + + (endMarkerIndex > startMarkerIndex && endMarkerIndex < body.length + ? body.slice(endMarkerIndex) + : ""); +} + +const invalidCommitReason = Object.entries( + participationResult.invalidCommits ?? {} +) + .map( + ([sha, reason]) => + `Commit ${sha} won't be taken into account for the following reason: ${reason}` + ) + .join("\n\n"); + +console.log( + JSON.stringify({ + body, + hasFailures: Boolean(invalidCommitReason), + invalidCommitReason, + }) +); From 7d11f97d1534167655947bd081c34a0755813c28 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 14 Jun 2023 13:15:45 +0200 Subject: [PATCH 2/9] fixup! Add vote lifecycle workflows --- .github/workflows/closeVote.yml | 19 +++++++++---------- .github/workflows/initiateNewVote.yml | 14 +++++++------- .github/workflows/watchVote.yml | 8 ++++---- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.github/workflows/closeVote.yml b/.github/workflows/closeVote.yml index 0851e8c4..806e4133 100644 --- a/.github/workflows/closeVote.yml +++ b/.github/workflows/closeVote.yml @@ -1,12 +1,13 @@ name: Close vote on: - issue_comment: - types: [created] + # 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" + description: ID of the Vote PR that contains a vote ready to be closed required: true type: number @@ -17,7 +18,7 @@ permissions: jobs: close-vote: if: github.event.inputs.pr || - (github.event.issue.pull_request && contains(github.event.comment.body, '-----BEGIN SHAMIR KEY PART-----')) + (github.event.issue.pull_request && contains(github.event.comment.body, '-----BEGIN SHAMIR KEY PART-----')) runs-on: ubuntu-latest steps: - name: Get PR URL @@ -26,16 +27,14 @@ jobs: 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 + 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 + run: gh pr view ${{ steps.pr-url.outputs.URL }} --json headRefName --jq '"head=" + .headRefName' >> "$GITHUB_OUTPUT" env: GH_TOKEN: ${{ github.token }} @@ -47,12 +46,12 @@ jobs: echo "minusOne=$(($NB_OF_COMMITS - 1))" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ github.token }} - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - 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 + 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 >> diff --git a/.github/workflows/initiateNewVote.yml b/.github/workflows/initiateNewVote.yml index 5bf2ada9..79018d04 100644 --- a/.github/workflows/initiateNewVote.yml +++ b/.github/workflows/initiateNewVote.yml @@ -21,14 +21,14 @@ jobs: repository-projects: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - 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" + - run: '! grep -q "subject: REPLACEME" votes/initiateNewVote.yml' - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* - name: Validate YAML and ensure there are more than 1 candidate @@ -44,7 +44,7 @@ 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 }}' gh pr edit ${{ github.event.pull_request.html_url }} --base 'initiateNewVote' env: GH_TOKEN: ${{ github.token }} @@ -56,11 +56,11 @@ jobs: repository-projects: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: - persist-credentials: true # we need the credentials to push the new vote branch + 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 + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3.6.0 with: node-version: lts/* - name: Extract info from the pushed file diff --git a/.github/workflows/watchVote.yml b/.github/workflows/watchVote.yml index d23ce274..e9c15ec2 100644 --- a/.github/workflows/watchVote.yml +++ b/.github/workflows/watchVote.yml @@ -3,7 +3,7 @@ name: Validate vote commit and update participation on: pull_request: types: [synchronize] - paths: ["votes/**"] + paths: [votes/**] concurrency: ${{ github.workflow }}--${{ github.head_ref }} permissions: @@ -21,7 +21,7 @@ jobs: 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 + - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 with: fetch-depth: ${{ steps.nb-of-commits.outputs.plusOne }} persist-credentials: false @@ -46,7 +46,7 @@ jobs: "$GITHUB_OUTPUT"; cat "$GITHUB_OUTPUT" env: SUBPATH: ${{ github.head_ref }} - FIRST_COMMIT_REF: "HEAD^2~${{ steps.nb-of-commits.outputs.minusOne }}" + 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}} @@ -67,4 +67,4 @@ jobs: ${{ 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" + run: 'false' From a36e16eed013cdde0151a32213e8ed9162377481 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 14 Jun 2023 13:18:04 +0200 Subject: [PATCH 3/9] fixup! Add vote lifecycle workflows --- votes/initiateNewVote/generateNewVotePR.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/votes/initiateNewVote/generateNewVotePR.mjs b/votes/initiateNewVote/generateNewVotePR.mjs index f88064bc..cf4a4e00 100755 --- a/votes/initiateNewVote/generateNewVotePR.mjs +++ b/votes/initiateNewVote/generateNewVotePR.mjs @@ -168,7 +168,7 @@ for await (const member of readReadme(readLines({ input, crlfDelay }))) { input.destroy?.(); -const shareholderThreshold = 2; // Math.ceil(tscMembersArray.length / 4); +const shareholdersThreshold = 3; // Math.ceil(tscMembersArray.length / 4); const keyServerURL = "hkps://keys.openpgp.org"; @@ -189,7 +189,7 @@ await generateNewVoteFolder({ baseBranch: "main", forceClone: !argv["tsc-repository-path"], }, - shareholdersThreshold: shareholderThreshold, + shareholdersThreshold, shareholders: tscMembersArray.map(({ email }) => email), allowedVoters: tscMembersArray.map( (voter) => `${voter.name} <${voter.email}>` From 23b81146c3371b5e5b3f666b1bb8c90fbcefe896 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Wed, 14 Jun 2023 16:49:59 +0200 Subject: [PATCH 4/9] lint --- .github/workflows/watchVote.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/watchVote.yml b/.github/workflows/watchVote.yml index e9c15ec2..f44fd2c9 100644 --- a/.github/workflows/watchVote.yml +++ b/.github/workflows/watchVote.yml @@ -46,7 +46,7 @@ jobs: "$GITHUB_OUTPUT"; cat "$GITHUB_OUTPUT" env: SUBPATH: ${{ github.head_ref }} - FIRST_COMMIT_REF: 'HEAD^2~${{ steps.nb-of-commits.outputs.minusOne }}' + 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}} From 7b7253f22e44e27868c634fa7f5f756d7026db9d Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 29 Jun 2023 01:18:50 +0200 Subject: [PATCH 5/9] Apply suggestions from code review --- .github/workflows/closeVote.yml | 2 +- .github/workflows/initiateNewVote.yml | 2 +- .github/workflows/watchVote.yml | 2 +- votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs | 2 +- votes/initiateNewVote/generateNewVotePR.mjs | 4 +++- votes/initiateNewVote/getVoteStatus.mjs | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/closeVote.yml b/.github/workflows/closeVote.yml index 806e4133..49516b55 100644 --- a/.github/workflows/closeVote.yml +++ b/.github/workflows/closeVote.yml @@ -65,7 +65,7 @@ jobs: git fetch origin '${{ steps.branch.outputs.head }}' git reset FETCH_HEAD --mixed git checkout HEAD -- '${{ steps.branch.outputs.head }}' - - run: npm install @aduh95/caritat + - run: npm install @node-core/caritat - name: Attempt closing the vote id: vote-summary run: | diff --git a/.github/workflows/initiateNewVote.yml b/.github/workflows/initiateNewVote.yml index 79018d04..e870b303 100644 --- a/.github/workflows/initiateNewVote.yml +++ b/.github/workflows/initiateNewVote.yml @@ -74,7 +74,7 @@ jobs: - name: Reset to the base branch run: git fetch origin HEAD && git reset FETCH_HEAD --hard - name: Install npm dependencies - run: npm install @aduh95/caritat + run: npm install @node-core/caritat - name: Configure git run: | git config --global user.email "github-bot@iojs.org" diff --git a/.github/workflows/watchVote.yml b/.github/workflows/watchVote.yml index f44fd2c9..ef470d02 100644 --- a/.github/workflows/watchVote.yml +++ b/.github/workflows/watchVote.yml @@ -29,7 +29,7 @@ jobs: run: curl -L https://raw.githubusercontent.com/nodejs/node/main/.mailmap >> .mailmap - - run: npm install @aduh95/caritat + - run: npm install @node-core/caritat - name: Get PR description id: desc run: | diff --git a/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs b/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs index daf8527d..e04475b6 100755 --- a/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs +++ b/votes/initiateNewVote/decryptPrivateKeyAndCloseVote.mjs @@ -6,7 +6,7 @@ import { createInterface as readLines } from "node:readline"; import { fileURLToPath } from "node:url"; import { parseArgs } from "node:util"; -import countFromGit from "@aduh95/caritat/countBallotsFromGit"; +import countFromGit from "@node-core/caritat/countBallotsFromGit"; const { values: parsedArgs } = parseArgs({ options: { diff --git a/votes/initiateNewVote/generateNewVotePR.mjs b/votes/initiateNewVote/generateNewVotePR.mjs index cf4a4e00..df532b61 100755 --- a/votes/initiateNewVote/generateNewVotePR.mjs +++ b/votes/initiateNewVote/generateNewVotePR.mjs @@ -9,7 +9,7 @@ import { open, readFile } from "node:fs/promises"; import { parseArgs } from "node:util"; import { spawn } from "node:child_process"; -import generateNewVoteFolder from "@aduh95/caritat/generateNewVoteFolder"; +import generateNewVoteFolder from "@node-core/caritat/generateNewVoteFolder"; import readReadme from "./extractInfoFromReadme.mjs"; @@ -256,6 +256,8 @@ Vote instructions: To close the vote, at least ${shareholderThreshold} 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. diff --git a/votes/initiateNewVote/getVoteStatus.mjs b/votes/initiateNewVote/getVoteStatus.mjs index 99d02705..7af8fe15 100755 --- a/votes/initiateNewVote/getVoteStatus.mjs +++ b/votes/initiateNewVote/getVoteStatus.mjs @@ -2,7 +2,7 @@ import { exit } from "node:process"; import { env } from "node:process"; -import count from "@aduh95/caritat/countParticipationFromGit"; +import count from "@node-core/caritat/countParticipationFromGit"; const START_MARKER = ""; const END_MARKER = ""; From fc32cacd5cea986b91f289c750487921b8f8978f Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Fri, 17 Nov 2023 08:52:15 -0500 Subject: [PATCH 6/9] Update votes/initiateNewVote/_EDIT_ME.yml Co-authored-by: Antoine du Hamel --- votes/initiateNewVote/_EDIT_ME.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/votes/initiateNewVote/_EDIT_ME.yml b/votes/initiateNewVote/_EDIT_ME.yml index d0d5ebe1..d7b1c85a 100644 --- a/votes/initiateNewVote/_EDIT_ME.yml +++ b/votes/initiateNewVote/_EDIT_ME.yml @@ -10,7 +10,7 @@ headerInstructions: | Please set a score to 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 tied two or more proposals if you have no preference. + 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 From a2def685b954d06cbd990f702abe8d06a0f3aff2 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 9 Nov 2023 18:56:32 +0100 Subject: [PATCH 7/9] use a more generic CLI flag --- .github/workflows/initiateNewVote.yml | 2 +- votes/initiateNewVote/generateNewVotePR.mjs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/initiateNewVote.yml b/.github/workflows/initiateNewVote.yml index e870b303..a47847d9 100644 --- a/.github/workflows/initiateNewVote.yml +++ b/.github/workflows/initiateNewVote.yml @@ -101,7 +101,7 @@ jobs: ./votes/initiateNewVote/generateNewVotePR.mjs \ --remote origin \ --github-repo-name '${{ github.repository }}' \ - --tsc-repository-path . \ + --vote-repository-path . \ --branch '${{ steps.data.outputs.branchName }}' \ --subject '${{ fromJSON(steps.data.outputs.json_data).subject }}' \ --candidate '${{ join(fromJSON(steps.data.outputs.json_data).candidates, ''' --candidate ''') }}' \ diff --git a/votes/initiateNewVote/generateNewVotePR.mjs b/votes/initiateNewVote/generateNewVotePR.mjs index df532b61..8f880919 100755 --- a/votes/initiateNewVote/generateNewVotePR.mjs +++ b/votes/initiateNewVote/generateNewVotePR.mjs @@ -45,11 +45,11 @@ const { values: argv } = parseArgs({ type: "string", short: "r", }, - "tsc-repository-path": { + "vote-repository-path": { type: "string", short: "R", description: - "Path to the local nodejs/TSC repository. If not provided, it will be fetched from GitHub", + "Path to the local vote repository. If not provided, it will be fetched from GitHub", }, subject: { type: "string", @@ -112,7 +112,7 @@ if (argv.help) { "nodejs/node. If not provided, files will be downloaded from HTTPS." ); console.log( - "\t--tsc-repository-path (alias -R): Path to a local clone of " + + "\t--vote-repository-path (alias -R): Path to a local clone of " + "nodejs/TSC. If not provided, it will be cloned from SSH (or HTTPS if " + "an HTTPS remote is provided)." ); @@ -187,7 +187,7 @@ await generateNewVoteFolder({ repo: argv.remote ?? `git@github.com:${argv["github-repo-name"]}.git`, branch: argv.branch, baseBranch: "main", - forceClone: !argv["tsc-repository-path"], + forceClone: !argv["vote-repository-path"], }, shareholdersThreshold, shareholders: tscMembersArray.map(({ email }) => email), @@ -195,8 +195,8 @@ await generateNewVoteFolder({ (voter) => `${voter.name} <${voter.email}>` ), method: "Condorcet", - path: argv["tsc-repository-path"] - ? resolve(argv["tsc-repository-path"], argv.branch) + path: argv["vote-repository-path"] + ? resolve(argv["vote-repository-path"], argv.branch) : argv.branch, }); From 20505fcceec10b4e6be3fb35c3b6e2d41344370f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 21 Nov 2023 01:27:00 +0100 Subject: [PATCH 8/9] fix remaining issues --- .github/workflows/initiateNewVote.yml | 43 ++++++++++++++++----- votes/initiateNewVote/generateNewVotePR.mjs | 8 ++-- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/workflows/initiateNewVote.yml b/.github/workflows/initiateNewVote.yml index a47847d9..674fc67d 100644 --- a/.github/workflows/initiateNewVote.yml +++ b/.github/workflows/initiateNewVote.yml @@ -71,6 +71,23 @@ jobs: 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< `- ${name} @${handle} (TSC)`) .join("\n")} -To close the vote, a minimum of ${shareholderThreshold} key parts would need to be revealed. +To close the vote, a minimum of ${shareholdersThreshold} key parts would need to be revealed. Vote instructions will follow.`, "--jq", @@ -253,7 +253,7 @@ Vote instructions: - on the CLI: ${"`"}git node vote ${prUrl}${"`"} - on the web: -To close the vote, at least ${shareholderThreshold} secret holder(s)[^1] must \ +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 From 23eb272b968e33e5f77bf3238c8f792712fdd257 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Tue, 21 Nov 2023 01:31:35 +0100 Subject: [PATCH 9/9] Update votes/initiateNewVote/_EDIT_ME.yml --- votes/initiateNewVote/_EDIT_ME.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/votes/initiateNewVote/_EDIT_ME.yml b/votes/initiateNewVote/_EDIT_ME.yml index d7b1c85a..c9fac556 100644 --- a/votes/initiateNewVote/_EDIT_ME.yml +++ b/votes/initiateNewVote/_EDIT_ME.yml @@ -7,7 +7,7 @@ subject: REPLACEME # 2. You can leave the header instructions as is, or modify them if you see fit. headerInstructions: | - Please set a score to proposal according to your preferences. + 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.