diff --git a/.github/founder-roster.json b/.github/founder-roster.json new file mode 100644 index 0000000..172f19d --- /dev/null +++ b/.github/founder-roster.json @@ -0,0 +1,5 @@ +{ + "issue_number": 3, + "policy_commit": "d56279b4f3c25dd8f9e68699d61be23a0c7a6b07", + "founder_logins": ["a-51-50", null, null] +} diff --git a/.github/workflows/founder-adoption.yml b/.github/workflows/founder-adoption.yml new file mode 100644 index 0000000..826f976 --- /dev/null +++ b/.github/workflows/founder-adoption.yml @@ -0,0 +1,47 @@ +name: Verify and close founder agreement + +on: + issue_comment: + types: [created, edited, deleted] + issues: + types: [edited, reopened] + push: + branches: [main] + paths: + - '.github/founder-roster.json' + - '.github/workflows/founder-adoption.yml' + - 'scripts/founder-adoption*.mjs' + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: opendkg-founder-issue-3 + cancel-in-progress: false + +jobs: + verify: + if: >- + github.repository == 'OpenDKG/.github' && + (github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event.issue.number == 3 && !github.event.issue.pull_request)) + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout trusted default branch + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Set up Node 22 + uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Verify adoption validation logic + run: node --test scripts/founder-adoption-validator.test.mjs + - name: Read founder acceptances and update adoption issue if appropriate + run: node scripts/founder-adoption.mjs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-founder-adoption.yml b/.github/workflows/test-founder-adoption.yml new file mode 100644 index 0000000..e5fb200 --- /dev/null +++ b/.github/workflows/test-founder-adoption.yml @@ -0,0 +1,33 @@ +name: Test founder adoption automation + +on: + pull_request: + paths: + - '.github/founder-roster.json' + - '.github/workflows/founder-adoption.yml' + - '.github/workflows/test-founder-adoption.yml' + - 'scripts/founder-adoption*.mjs' + push: + branches: [main] + paths: + - '.github/founder-roster.json' + - '.github/workflows/founder-adoption.yml' + - '.github/workflows/test-founder-adoption.yml' + - 'scripts/founder-adoption*.mjs' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Run founder acceptance unit tests + run: node --test scripts/founder-adoption-validator.test.mjs diff --git a/FOUNDER_ADOPTION_AUTOMATION.md b/FOUNDER_ADOPTION_AUTOMATION.md new file mode 100644 index 0000000..82cadca --- /dev/null +++ b/FOUNDER_ADOPTION_AUTOMATION.md @@ -0,0 +1,44 @@ +# Automatic closing of the three-cofounder adoption issue + +## What it does + +The GitHub Actions workflow `.github/workflows/founder-adoption.yml` watches comments on [Issue #3](https://github.com/OpenDKG/.github/issues/3), as well as edits/deletions, issue reopening, and changes to the roster. + +It **only** marks the founding agreement adopted when **three specific, pre-registered, distinct personal GitHub accounts** have each publicly posted the **exact required declaration**, personally accepting the **same pinned** Contributor Agreement and Governance revision: + +`d56279b4f3c25dd8f9e68699d61be23a0c7a6b07`. + +The automation checks the stable GitHub **numeric account IDs** behind the roster handles, and the **currently visible** comment text. It will ignore an extra or impersonating comment, a GitHub bot, another policy revision, or a different Issue number. It does not claim to verify civil identities or the legal validity of a signature. + +Once all three pass, it: +1. Writes a public completion receipt listing each acceptance comment permalink, GitHub user ID, SHA-256 comment-body fingerprint, policy revision, and UTC verification timestamp. +2. Adds label `founders:agreement-complete`. +3. **Closes Issue #3** as completed. + +If a signature is **later edited or deleted**, the same workflow reassesses the three accounts. If a formerly completed issue no longer has all three currently valid declarations, it **reopens Issue #3**, removes the completed label, and writes a warning. A completion receipt is a *historical observation*, not an immutable or legally qualified e-signature; administrators can still change GitHub issue state manually. GitHub's API and Git history do not make comments undeletable. + +## Initial configuration — required before it can auto-close + +The committed configuration [`.github/founder-roster.json`](.github/founder-roster.json) starts with three reserved seats: + +```json +{ + "issue_number": 3, + "policy_commit": "d56279b4f3c25dd8f9e68699d61be23a0c7a6b07", + "founder_logins": ["a-51-50", null, null] +} +``` + +The **two null entries intentionally prevent automatic closing**. Once the two other *original equal cofounders* give you their actual GitHub usernames, open a reviewed PR that replaces **only those two null values** with their handles, e.g., `"cofounder-two"` and `"cofounder-three"`. Do **not** invent account names, use GitHub profile display names, or fill the seats with a random contributor. Keep the three original persons' identities distinct. + +After roster change merges into `main`, a `push` event automatically checks already-posted comments; a new or edited comment on Issue #3 also triggers it. The three founders **do not need to be members of OpenDKG to post** on the public Issue, and no additional acceptance is required from `@a-51-50` if the **existing exact-revision declaration remains valid**. + +Do not edit the policy commit in the roster just to bypass missing consent. If substantive policy terms change, arrange a separate new version with each person's explicit approval; never auto-count an older acceptance as agreement to new terms. + +## Access and permissions + +The workflow **never sends GitHub organization invitations, grants Repository Admin/Owner roles, or assigns legal liability**. Those changes still require explicit authorized organizational action. The three original cofounders retain equal intended governance standing regardless of who created the GitHub organization, and ordinary future contributors do not acquire founder rights. + +GitHub Actions uses the repository-scoped default `GITHUB_TOKEN` with `contents: read` and `issues: write`; no personal token, dedicated external server, subscription, or third-party service is required for this task. The workflow runs only on trusted `main` code; comment text is data and never executed as a shell command. + +The main requirement is to keep the [Issue #3](https://github.com/OpenDKG/.github/issues/3) available and preserve the public acceptance records. For stronger proof, export the receipt and public evidence separately after completion, and obtain appropriate legal advice before relying on it as a binding cross-jurisdiction founder agreement. diff --git a/FOUNDER_ONBOARDING.md b/FOUNDER_ONBOARDING.md index 8e8d74e..7147024 100644 --- a/FOUNDER_ONBOARDING.md +++ b/FOUNDER_ONBOARDING.md @@ -24,6 +24,12 @@ These are **three equal founding seats**. Who created the GitHub account first d 6. After unanimous adoption, assign GitHub privileges by an explicit, documented founders' decision. If all three are meant to have equal technical control, **Organization Owner** for all three is more equivalent than giving two people only **Repository Admin**. But GitHub Owners have powerful unilateral platform capabilities, so all three should understand this risk and use MFA, separate recovery methods, least privilege and review controls. This process **does not automatically grant any account Owner or Admin**. 7. Ordinary later contributors apply via [the membership form](https://github.com/OpenDKG/.github/issues/new?template=membership-agreement.yml) or submit PRs as external contributors. They have **no cofounder status**, no governance vote and no automatic admin permissions. +## Automatic verification once the three usernames are known + +The [founder adoption automation](FOUNDER_ADOPTION_AUTOMATION.md) will verify and close Issue #3 when all three **specific** original GitHub accounts have personally accepted the **exact same policy revision**. It records a verification receipt. It cannot run to completion until the two original cofounders' actual GitHub handles replace the placeholders in [the founder roster](.github/founder-roster.json) through a reviewed PR. The first account's existing valid acceptance is preserved; do not require `@a-51-50` to sign again without a material policy change. + +Issue comments can still be edited or deleted. On such a change after automatic closure, the bot will reopen Issue #3 if any previously verified acceptance is no longer currently present. This is an integrity check, not an immutable legal e-signature. No GitHub privileges are granted automatically. + ## Accountability and limits No statement in GitHub governance makes the three persons automatically jointly liable for each other's activities, automatically shields `@a-51-50` from liability, or turns a GitHub organization into a limited-liability company. This must be addressed separately under the relevant jurisdictions and in actual contracts, if any. Do not represent an unregistered collaboration as an LLC. diff --git a/README.md b/README.md index 89f72c8..e809f66 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ OpenDKG was started as a joint effort of **three equal cofounders**. The fact th - [Founding acceptance and onboarding](FOUNDER_ONBOARDING.md) - [Founding adoption record (Issue #3)](https://github.com/OpenDKG/.github/issues/3) +- [Automated final verification and Issue closing](FOUNDER_ADOPTION_AUTOMATION.md) — checks three pre-registered accounts; inactive while two founder handles remain unconfigured. Future non-founder members use the separate membership form. No one is automatically promoted to admin/Owner based on a form or comment. Equal cofounder status does not automatically transfer assets or divide statutory liability. diff --git a/scripts/founder-adoption-validator.mjs b/scripts/founder-adoption-validator.mjs new file mode 100644 index 0000000..06b9947 --- /dev/null +++ b/scripts/founder-adoption-validator.mjs @@ -0,0 +1,92 @@ +// OpenDKG three-founder adoption: pure, dependency-free checks. +import { createHash } from "node:crypto"; + +export const POLICY_COMMIT = "d56279b4f3c25dd8f9e68699d61be23a0c7a6b07"; +export const FOUNDER_ISSUE = 3; +export const COMPLETE_LABEL = "founders:agreement-complete"; + +// Match the exact declaration displayed in Issue #3 at the pinned policy revision. +// Allow only Markdown quote/code styling and whitespace normalization. +export const DECLARATION = + "I confirm that I am one of the three original equal cofounders of OpenDKG. " + + "I have read and affirmatively accept OpenDKG Contributor Agreement v1.0 and Governance v1.0 at commit " + + POLICY_COMMIT + ". " + + "I accept the scope covering present and future OpenDKG repositories. " + + "I understand that the sequence of GitHub invitations does not give one cofounder superior status; " + + "my agreement does not transfer third-party assets or automatically allocate or eliminate personal legal liability. " + + "I consent to this public GitHub acceptance record under my own account. " + + "I understand prior code is not retroactively licensed without required permission."; + +export function normalizeDeclaration(body) { + if (typeof body !== "string") return ""; + return body.replace(/^\s*>\s?/gm, "").replace(/\x60/g, "").replace(/\s+/g, " ").trim(); +} + +const loginPattern = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/; + +export function validateRoster(config) { + const errors = []; + if (config?.issue_number !== FOUNDER_ISSUE) errors.push("Issue number must be exactly 3."); + if (config?.policy_commit !== POLICY_COMMIT) errors.push("Policy commit mismatch."); + const logins = config?.founder_logins; + if (!Array.isArray(logins) || logins.length !== 3) { + errors.push("Exactly three founder seats must exist."); + return { ready: false, errors, logins: [] }; + } + if (logins[0] !== "a-51-50") errors.push("Original custodian account must occupy the first founding seat."); + const active = logins.filter((login) => typeof login === "string" && login.length > 0); + for (const login of active) { + if (!loginPattern.test(login)) errors.push("Invalid GitHub login in roster."); + } + if (new Set(active.map((login) => login.toLowerCase())).size !== active.length) { + errors.push("Founding account logins must be distinct."); + } + // Null/empty handles are intentional placeholders. Do not close while any remain. + const ready = errors.length === 0 && active.length === 3; + return { ready, errors, logins: ready ? logins : [] }; +} + +export function evaluateFounderAcceptance(config, githubAccounts, issueComments) { + const roster = validateRoster(config); + if (!roster.ready) return { complete: false, configured: false, missing: [], records: [], errors: roster.errors }; + const errors = []; + const records = []; + const missing = []; + const ids = new Set(); + for (const login of roster.logins) { + const user = githubAccounts[login.toLowerCase()]; + if (!user || user.type !== "User" || !Number.isSafeInteger(user.id) || user.id <= 0) { + errors.push("GitHub user not found or not a human user: " + login); + missing.push(login); + continue; + } + if (ids.has(user.id)) { + errors.push("Two roster entries resolved to the same GitHub identity."); + missing.push(login); + continue; + } + ids.add(user.id); + const valid = issueComments.filter((comment) => + Number.isSafeInteger(comment?.id) && + comment.user?.id === user.id && + comment.user?.type === "User" && + normalizeDeclaration(comment.body) === DECLARATION + ); + if (!valid.length) { + missing.push(login); + continue; + } + const chosen = valid.sort((a, b) => a.id - b.id)[0]; + records.push({ + login: user.login, + user_id: user.id, + comment_id: chosen.id, + comment_url: chosen.html_url || "https://github.com/OpenDKG/.github/issues/3#issuecomment-" + chosen.id, + created_at: chosen.created_at || null, + updated_at: chosen.updated_at || null, + declaration_sha256: createHash("sha256").update(chosen.body, "utf8").digest("hex") + }); + } + return { complete: missing.length === 0 && errors.length === 0 && records.length === 3, + configured: true, missing, records, errors }; +} diff --git a/scripts/founder-adoption-validator.test.mjs b/scripts/founder-adoption-validator.test.mjs new file mode 100644 index 0000000..1013c42 --- /dev/null +++ b/scripts/founder-adoption-validator.test.mjs @@ -0,0 +1,110 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + POLICY_COMMIT, DECLARATION, normalizeDeclaration, + validateRoster, evaluateFounderAcceptance +} from "./founder-adoption-validator.mjs"; + +const configured = { + issue_number: 3, + policy_commit: POLICY_COMMIT, + founder_logins: ["a-51-50", "founder-two", "founder-three"] +}; +const people = { + "a-51-50": { login: "a-51-50", id: 51213010, type: "User" }, + "founder-two": { login: "founder-two", id: 203, type: "User" }, + "founder-three": { login: "founder-three", id: 304, type: "User" } +}; +function signingComment(login, id, body = DECLARATION) { + const user = people[login]; + return { id, user, body, created_at: "2026-11-05T11:00:00Z", + html_url: "https://github.com/OpenDKG/.github/issues/3#issuecomment-" + id }; +} +const comments = [ + signingComment("a-51-50", 51), + signingComment("founder-two", 52), + signingComment("founder-three", 53) +]; + +test("unconfigured two founder seats are fail-closed", () => { + const r = evaluateFounderAcceptance({ + ...configured, founder_logins: ["a-51-50", null, null] + }, people, comments); + assert.equal(r.configured, false); + assert.equal(r.complete, false); +}); + +test("valid configured roster needs exactly three distinct account handles", () => { + assert.equal(validateRoster(configured).ready, true); + assert.equal(validateRoster({ ...configured, founder_logins: ["a-51-50", "founder-two", "FOUNDER-TWO"] }).ready, false); + assert.equal(validateRoster({ ...configured, founder_logins: ["someone-else", "founder-two", "founder-three"] }).ready, false); + assert.equal(validateRoster({ ...configured, founder_logins: ["a-51-50", "x"] }).ready, false); +}); + +test("rejects wrong policy revision or issue number", () => { + assert.equal(validateRoster({ ...configured, policy_commit: "old" }).ready, false); + assert.equal(validateRoster({ ...configured, issue_number: 4 }).ready, false); +}); + +test("three different human GitHub accounts signing the exact statement can complete", () => { + const result = evaluateFounderAcceptance(configured, people, comments); + assert.equal(result.complete, true); + assert.deepEqual(result.missing, []); + assert.deepEqual(result.records.map((x) => x.user_id), [51213010, 203, 304]); + assert.ok(result.records.every((x) => x.declaration_sha256.length === 64)); +}); + +test("current original custodian acceptance is accepted without forcing another signature", () => { + const result = evaluateFounderAcceptance(configured, people, [comments[0]]); + assert.equal(result.complete, false); + assert.deepEqual(result.records.map((x) => x.login), ["a-51-50"]); + assert.deepEqual(result.missing, ["founder-two", "founder-three"]); +}); + +test("not any three commenters: account mismatch never counts", () => { + const bad = { ...comments[2], user: { login: "unlisted", id: 999, type: "User" } }; + const result = evaluateFounderAcceptance(configured, people, [comments[0], comments[1], bad]); + assert.equal(result.complete, false); + assert.deepEqual(result.missing, ["founder-three"]); +}); + +test("rejects fabricated GitHub identity with same login but different user ID", () => { + const bad = { ...comments[2], user: { login: "founder-three", id: 900, type: "User" } }; + assert.equal(evaluateFounderAcceptance(configured, people, [comments[0], comments[1], bad]).complete, false); +}); + +test("rejects one account filling two founder seats, even if names differ", () => { + const badAccounts = { ...people, "founder-three": { ...people["founder-three"], id: 203 } }; + const result = evaluateFounderAcceptance(configured, badAccounts, comments); + assert.equal(result.complete, false); + assert.ok(result.errors.some((x) => x.includes("same GitHub identity"))); +}); + +test("edited or deleted declaration no longer counts", () => { + const edited = { ...comments[2], body: DECLARATION.replace("affirmatively accept", "might accept") }; + const changed = evaluateFounderAcceptance(configured, people, [comments[0], comments[1], edited]); + assert.equal(changed.complete, false); + assert.equal(evaluateFounderAcceptance(configured, people, comments.slice(0, 2)).complete, false); +}); + +test("comment with older policy revision is not current acceptance", () => { + const stale = { ...comments[1], body: DECLARATION.replace(POLICY_COMMIT, "2e429d904db1059a678085792ad44f1d732d7ea1") }; + assert.equal(evaluateFounderAcceptance(configured, people, [comments[0], stale, comments[2]]).complete, false); +}); + +test("the declaration may be quoted/formatting-wrapped but must be otherwise exact", () => { + const normalized = normalizeDeclaration("> " + DECLARATION.replace(POLICY_COMMIT, "\x60" + POLICY_COMMIT + "\x60")); + assert.equal(normalized, DECLARATION); + assert.equal(normalizeDeclaration(DECLARATION + " extra"), DECLARATION + " extra"); +}); + +test("bot accounts are not founding signers", () => { + const badAccounts = { ...people, "founder-three": { ...people["founder-three"], type: "Bot" } }; + assert.equal(evaluateFounderAcceptance(configured, badAccounts, comments).complete, false); +}); + +test("wrong actor's comment never counts for another founding seat", () => { + const impostor = { ...comments[1], user: people["founder-three"] }; + const result = evaluateFounderAcceptance(configured, people, [comments[0], impostor]); + assert.equal(result.complete, false); +}); diff --git a/scripts/founder-adoption.mjs b/scripts/founder-adoption.mjs new file mode 100644 index 0000000..0413130 --- /dev/null +++ b/scripts/founder-adoption.mjs @@ -0,0 +1,124 @@ +// GitHub Actions: close Issue #3 only after three pre-designated founders accept. +import { readFileSync } from "node:fs"; +import { createHash } from "node:crypto"; +import { validateRoster, evaluateFounderAcceptance, POLICY_COMMIT, FOUNDER_ISSUE, COMPLETE_LABEL } from "./founder-adoption-validator.mjs"; + +if (process.env.GITHUB_REPOSITORY !== "OpenDKG/.github") throw new Error("Unexpected repository."); +const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); +const eventName = process.env.GITHUB_EVENT_NAME; +if ((eventName === "issue_comment" || eventName === "issues") && + (event.issue?.number !== FOUNDER_ISSUE || event.issue?.pull_request)) process.exit(0); +if (eventName === "issue_comment" && event.comment?.user?.type === "Bot") process.exit(0); + +const roster = JSON.parse(readFileSync(new URL("../.github/founder-roster.json", import.meta.url), "utf8")); +const configuration = validateRoster(roster); +if (configuration.errors.length) throw new Error("Invalid founder roster: " + configuration.errors.join("; ")); +if (!configuration.ready) { + console.log("Two founder account handles are not yet configured; no issue state or labels changed."); + process.exit(0); +} +const token = process.env.GITHUB_TOKEN; +if (!token) throw new Error("Missing GitHub Actions token."); +const prefix = "/repos/OpenDKG/.github"; +async function api(method, path, body) { + const res = await fetch("https://api.github.com" + path, { + method, + headers: { + Accept: "application/vnd.github+json", + Authorization: "Bearer " + token, + "X-GitHub-Api-Version": "2022-11-28", + ...(body === undefined ? {} : { "Content-Type": "application/json" }) + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }) + }); + if (!res.ok) { + const explanation = (await res.text()).slice(0, 500); + const err = new Error(method + " " + path + ": HTTP " + res.status + " " + explanation); + err.status = res.status; + throw err; + } + return res.status === 204 ? null : res.json(); +} + +const issue = await api("GET", prefix + "/issues/" + FOUNDER_ISSUE); +if (issue.pull_request || !String(issue.body || "").includes("**Exact agreement revision:** " + + String.fromCharCode(96) + POLICY_COMMIT + String.fromCharCode(96))) { + throw new Error("Founding issue does not reference the expected immutable agreement revision."); +} + +// Resolve each handle to a stable GitHub user ID. No wildcard acceptance. +const accounts = {}; +for (const login of configuration.logins) { + accounts[login.toLowerCase()] = await api("GET", "/users/" + encodeURIComponent(login)); +} +const comments = []; +for (let page = 1; page <= 50; page++) { + const results = await api("GET", prefix + "/issues/" + FOUNDER_ISSUE + "/comments?per_page=100&page=" + page); + comments.push(...results); + if (results.length < 100) break; + if (page === 50) throw new Error("Too many issue comments; refusing to make an incomplete adoption decision."); +} +const result = evaluateFounderAcceptance(roster, accounts, comments); +if (result.errors.length) throw new Error("Founder verification error: " + result.errors.join("; ")); + +const labels = new Set((issue.labels || []).map((x) => x.name)); +const hadCompletionLabel = labels.has(COMPLETE_LABEL); +const signatureSetHash = createHash("sha256").update(JSON.stringify(result.records.map((r) => [r.user_id, r.comment_id, r.declaration_sha256]))).digest("hex"); +const marker = ""; +const existingReceipt = comments.some((c) => + c.user?.login === "github-actions[bot]" && String(c.body || "").includes(marker) +); + +if (!result.complete) { + console.log("Not all three original founders have a currently valid acceptance.", + JSON.stringify({ pending: result.missing, accepted: result.records.map((r) => r.login) })); + if (hadCompletionLabel) { + // Previously complete, but an acceptance was edited/deleted or roster changed. + if (issue.state === "closed") await api("PATCH", prefix + "/issues/" + FOUNDER_ISSUE, { state: "open" }); + await api("DELETE", prefix + "/issues/" + FOUNDER_ISSUE + "/labels/" + encodeURIComponent(COMPLETE_LABEL)); + await api("POST", prefix + "/issues/" + FOUNDER_ISSUE + "/comments", { + body: "### Founder acceptance requires renewed review\n\nA previously verified acceptance is now missing or no longer matches the recorded revision. The automation has reopened the issue and cleared its completion label. This does **not** revoke validly granted open-source licenses. Maintainers should review the remaining personal comments and verify all three identities again.\n\nPending accounts: " + result.missing.map((x) => "@" + x).join(", ") + }); + } + process.exit(0); +} + +if (!existingReceipt) { + const lines = [ + marker, + "### Three original cofounders — adoption verified", + "", + "The three accounts recorded in the checked-in founder roster each publicly posted the prescribed statement accepting **Contributor Agreement v1.0** and **Governance v1.0** at the same immutable revision.", + "", + "- Policy commit: " + POLICY_COMMIT, + "- Verification time (UTC): " + new Date().toISOString(), + "- Founder acceptance records:", + ...result.records.map((rec) => + " - @" + rec.login + " (GitHub user ID " + rec.user_id + "): " + + rec.comment_url + " — SHA-256 of current comment body: " + rec.declaration_sha256), + "", + "**Limitations:** The record verifies GitHub accounts and currently visible text, not civil identity, an uneditable qualified e-signature, a registered business, or an automatic assignment of copyrights. Repository access must still be granted separately by a GitHub organization owner. Historic open-source grants remain governed by their licenses." + ]; + await api("POST", prefix + "/issues/" + FOUNDER_ISSUE + "/comments", { body: lines.join("\n") }); +} +if (!hadCompletionLabel) { + const color = "0e8a16"; + try { + await api("GET", prefix + "/labels/" + encodeURIComponent(COMPLETE_LABEL)); + } catch (e) { + if (e.status !== 404) throw e; + try { + await api("POST", prefix + "/labels", { + name: COMPLETE_LABEL, color, + description: "Three rostered original cofounders accepted the same policy revision." + }); + } catch (createErr) { if (createErr.status !== 422) throw createErr; } + } + await api("POST", prefix + "/issues/" + FOUNDER_ISSUE + "/labels", { labels: [COMPLETE_LABEL] }); +} +if (issue.state !== "closed") { + await api("PATCH", prefix + "/issues/" + FOUNDER_ISSUE, { state: "closed", state_reason: "completed" }); + console.log("Founding acceptance complete: issue #3 closed."); +} else { + console.log("Founding issue #3 already closed; completion confirmed."); +}