|
Download .github/scripts/bounty/README.md from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 7.26 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/.github/scripts/bounty/README.md
- Command line
-
hf download hf://SaylorTwift/forgecode/.github/scripts/bounty/README.md
-
curl -L -o README.md https://huggingface.co/SaylorTwift/forgecode/resolve/main/.github/scripts/bounty/README.md
7.26 kB
| # Bounty Automation | |
| Automates the full lifecycle of issue bounties β from label propagation when a PR is opened, through claiming when work begins, to rewarding when a PR is merged. | |
| ## Flow | |
| ``` | |
| Issue created | |
| βββ maintainer adds bounty: $N label | |
| β | |
| βββ parse-sync-generic-bounty.ts β adds generic bounty label | |
| β | |
| βΌ | |
| Issue assigned to contributor | |
| βββ parse-sync-claimed.ts β adds bounty: claimed | |
| β | |
| βΌ | |
| Contributor opens PR with "Closes #N" / "Fixes #N" / "Resolves #N" | |
| βββ parse-propagate-label.ts β copies bounty: $N to PR | |
| β posts comment on issue: "PR #X opened by @author" | |
| β | |
| βΌ | |
| PR is merged | |
| βββ parse-mark-rewarded.ts β adds bounty: rewarded to PR | |
| β adds bounty: rewarded to linked issue(s) | |
| β removes bounty: claimed from linked issue(s) | |
| β | |
| βΌ | |
| Bounty lifecycle complete | |
| Removal path: | |
| maintainer removes last bounty: $N label | |
| βββ parse-sync-generic-bounty.ts β removes generic bounty label | |
| ``` | |
| ## Labels | |
| | Label | Applied to | Set by | | |
| | -------------------------------- | ---------- | ----------------------------------------------------------- | | |
| | `bounty: $100` β¦ `bounty: $5500` | Issue | Maintainer (manually) | | |
| | `bounty` | Issue | `parse-sync-generic-bounty.ts` on value label add/remove | | |
| | `bounty: claimed` | Issue | `parse-sync-claimed.ts` on assignment | | |
| | `bounty: rewarded` | Issue + PR | `parse-mark-rewarded.ts` on merge | | |
| Bounty values follow the Fibonacci sequence: **$100, $200, $300, $500, $800, $1300, $2100, $3400, $5500**. | |
| ## Pipeline | |
| Each workflow job runs a single Node process (`run.ts`) that executes all three stages in memory β no temp files, no shell piping: | |
| ``` | |
| parse β ParsedIntent (in memory) β plan β BatchPlan (in memory) β execute | |
| ``` | |
| **Stage 1 β parse** (`parse-*.ts`): Reads the GitHub Actions event payload from `GITHUB_EVENT_PATH` and returns a `ParsedIntent` object. Pure β makes no API calls. | |
| **Stage 2 β plan** (`plan.ts`): Receives the `ParsedIntent`, fetches current labels only for targets not already known from the event payload, diffs desired vs actual state, and returns a `BatchPlan`. Minimises API calls by reusing label data already present in the event. | |
| **Stage 3 β execute** (`execute.ts`): Receives the `BatchPlan` and applies all mutations. Label additions per target are sent as a single batched `POST /labels` call. Each removal is a separate `DELETE` (GitHub has no bulk-remove endpoint). Comments are posted last. | |
| This design means: | |
| - The parse stage is trivially unit-testable (pure function, no mocks needed). | |
| - The plan stage is testable with a minimal mock that only needs `getLabels`. | |
| - The execute stage is testable with a mock that tracks calls β no HTTP. | |
| - API calls are minimised: additions are batched per target; label state already in the event payload is never re-fetched. | |
| ## Scripts | |
| The workflow invokes a single orchestrator script (`run.ts`) per job. The parse, plan, and execute modules are called directly in the same Node process, passing objects in memory. | |
| ### `run.ts` | |
| The entrypoint used by `bounty.yml`. Accepts a `--script` flag to select which parse module to run, then calls `plan` and `execute` in sequence β all in-process. | |
| ```sh | |
| npx tsx .github/scripts/bounty/run.ts \ | |
| --script <parse-script> \ | |
| --repo <owner/repo> \ | |
| --token <github-token> \ | |
| [--pr <number> | --issue <number>] | |
| ``` | |
| ### `parse-propagate-label.ts` | |
| Triggered by: `pull_request` β opened, edited, reopened. | |
| 1. Parses the PR body for closing keywords (`closes`, `fixes`, `resolves`, case-insensitive). | |
| 2. Returns a `ParsedIntent` with a `labelCopies` field β the plan stage fetches each linked issue's labels and copies any `bounty: $N` ones onto the PR. | |
| 3. Includes a comment mutation per linked issue (the plan stage drops it if the issue has no bounty labels). | |
| ### `parse-sync-claimed.ts` | |
| Triggered by: `issues` β assigned, unassigned. | |
| - **assigned**: returns `add: ["bounty: claimed"]` if the issue has a `bounty: $N` label. | |
| - **unassigned**: returns `remove: ["bounty: claimed"]` only when no assignees remain. | |
| - Issue labels are already in the event payload and supplied as `knownLabels` β no extra fetch. | |
| ### `parse-sync-generic-bounty.ts` | |
| Triggered by: `issues` β labeled, unlabeled. | |
| Keeps the generic `bounty` label in sync with value labels. Inspects `event.label` (the label that just changed) and only acts when it matches `bounty: $`. | |
| - **labeled**: returns `add: ["bounty"]`. | |
| - **unlabeled**: returns `remove: ["bounty"]` only when no value labels remain (guards against mid-tier-swap removal when a maintainer swaps one value label for another). | |
| - Issue labels from the event are supplied as `knownLabels` β no extra fetch. | |
| ### `parse-mark-rewarded.ts` | |
| Triggered by: `pull_request_target` β closed (merged only). | |
| 1. Returns empty intent if the PR was not merged or has no `bounty: $N` label. | |
| 2. Returns `add: ["bounty: rewarded"]` for the PR (labels known from event, no fetch). | |
| 3. Parses the PR body for linked issues; returns `add: ["bounty: rewarded"], remove: ["bounty: claimed"]` for each (plan stage fetches their labels). | |
| Uses `pull_request_target` so the job has write access to issues and PRs from forks. | |
| ### `plan.ts` | |
| Receives a `ParsedIntent` from the parse stage. Resolves `labelCopies` by fetching source issue labels. Fetches current labels for any target not already in `knownLabels`. Filters out no-op adds and removes. Returns a `BatchPlan`. | |
| ### `execute.ts` | |
| Receives a `BatchPlan` from the plan stage. For each mutation: one batched `POST` for all additions, one `DELETE` per removal, one `POST` per comment. | |
| ## Shared Module | |
| `github-api.ts` defines: | |
| - Event payload types (`PullRequestEvent`, `IssuesEvent`) | |
| - Pipeline types (`ParsedIntent`, `BatchPlan`, `TargetMutation`) | |
| - The `GitHubApi` interface (injectable for testing) | |
| - `GitHubRestApi` β the production implementation using `node:https` | |
| All scripts import types from `github-api.ts` and accept a `GitHubApi` instance in their `run()` / `plan()` / `execute()` function signature, making every step independently mockable. | |
| ## Tests | |
| Unit tests live alongside each script (`*.test.ts`) and use Node's built-in `node:test` runner. | |
| ```sh | |
| npm run test:bounty | |
| ``` | |
| - Parse tests: pure β no mock needed, just call `parse()` with a synthetic event. | |
| - Plan and execute tests: use a mock `GitHubApi` that tracks calls and returns preset label lists. | |
| - The CLI entrypoint in each script (yargs parsing + `GITHUB_EVENT_PATH` read) is guarded behind an `import.meta.url` check so it does not execute on import. | |
| ## Workflow Source | |
| `bounty.yml` is auto-generated from Rust source in `crates/forge_ci`. Do not edit it by hand β modify `crates/forge_ci/src/workflows/bounty.rs` and regenerate with: | |
| ```sh | |
| cargo test -p forge_ci | |
| ``` | |