diff --git a/bounty-notes/issue-83-external-bounty-flow.md b/bounty-notes/issue-83-external-bounty-flow.md new file mode 100644 index 0000000..5c959ec --- /dev/null +++ b/bounty-notes/issue-83-external-bounty-flow.md @@ -0,0 +1,38 @@ +# External bounty flow verification for issue #83 + +Fixes #83. + +Issue #83 covers an externally-created bounty where the repository app was not installed at issue creation time. This contribution adds a deterministic fixture and validator for that path so the pending-approval and approved bounty states can be checked without depending on a live CodeBounty environment. + +## What is covered + +- Repository: `CodeBountyOrg/BountyTestRepository` +- Issue: `#83` +- Source: external sponsor +- No-app-installed path: `installation.app_installed === false` +- Pending state: `pending_maintainer_approval` +- Available state: `available` +- Required PR syntax: `fixes #83` +- Visible bounty amount: `$150 USD` + +## Validation commands + +```bash +node --check scripts/validate-codebounty-external-bounty.mjs +node scripts/validate-codebounty-external-bounty.mjs test-fixtures/codebounty-external-issue-83.json +python3 - <<'PY' +import json +from pathlib import Path +fixture = json.loads(Path('test-fixtures/codebounty-external-issue-83.json').read_text()) +note = Path('bounty-notes/issue-83-external-bounty-flow.md').read_text() +assert fixture['issue']['number'] == 83 +assert fixture['installation']['app_installed'] is False +assert fixture['bounty']['amount_usd'] == 150 +assert 'fixes #83' in note.lower() +PY +git diff --check +``` + +## Payout note + +The GitHub-visible bounty is treated as submitted-visible only. CodeBounty platform application and maintainer acceptance remain payout blockers until verified. diff --git a/scripts/validate-codebounty-external-bounty.mjs b/scripts/validate-codebounty-external-bounty.mjs new file mode 100644 index 0000000..255a5a3 --- /dev/null +++ b/scripts/validate-codebounty-external-bounty.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +import { readFileSync } from 'node:fs'; + +const assert = (condition, message) => { + if (!condition) { + throw new Error(message); + } +}; + +const loadPayload = (path) => { + try { + return JSON.parse(readFileSync(path, 'utf8')); + } catch (error) { + throw new Error(`Unable to read external bounty fixture at ${path}: ${error.message}`); + } +}; + +const payloadPath = process.argv[2] ?? 'test-fixtures/codebounty-external-issue-83.json'; +const payload = loadPayload(payloadPath); + +assert(payload.repository?.full_name === 'CodeBountyOrg/BountyTestRepository', 'repository.full_name must match the target repository'); +assert(payload.issue?.number === 83, 'issue.number must be 83'); +assert(payload.issue?.source === 'external_sponsor', 'issue.source must identify an external sponsor'); +assert(payload.installation?.app_installed === false, 'fixture must cover the no-app-installed path'); +assert(payload.bounty?.amount_usd === 150, 'bounty.amount_usd must stay at the announced $150'); +assert(payload.bounty?.currency === 'USD', 'bounty currency must be USD'); +assert(Array.isArray(payload.timeline) && payload.timeline.length >= 2, 'timeline must include pending and available states'); + +const states = payload.timeline.map((event) => event.status); +assert(states[0] === 'pending_maintainer_approval', 'first timeline state must be pending maintainer approval'); +assert(states.includes('available'), 'timeline must include the available state after approval'); + +const pending = payload.timeline.find((event) => event.status === 'pending_maintainer_approval'); +const available = payload.timeline.find((event) => event.status === 'available'); +assert(pending?.requires_maintainer_action === true, 'pending state must require maintainer action'); +assert(available?.requires_maintainer_action === false, 'available state must no longer require maintainer action'); +assert(available?.required_pr_syntax === 'fixes #83', 'available state must preserve required PR syntax'); + +const normalized = { + issue: payload.issue.number, + amount_usd: payload.bounty.amount_usd, + external_sponsor: payload.issue.source === 'external_sponsor', + no_app_installed_path: payload.installation.app_installed === false, + final_status: available.status, + required_pr_syntax: available.required_pr_syntax, +}; + +console.log(JSON.stringify(normalized, null, 2)); diff --git a/test-fixtures/codebounty-external-issue-83.json b/test-fixtures/codebounty-external-issue-83.json new file mode 100644 index 0000000..b19a424 --- /dev/null +++ b/test-fixtures/codebounty-external-issue-83.json @@ -0,0 +1,33 @@ +{ + "event": "external_bounty.created", + "repository": { + "full_name": "CodeBountyOrg/BountyTestRepository" + }, + "issue": { + "number": 83, + "title": "Creating issue as an external user without App Installed", + "source": "external_sponsor" + }, + "installation": { + "app_installed": false, + "expected_path": "maintainer_approval_required" + }, + "bounty": { + "amount_usd": 150, + "currency": "USD", + "provider": "CodeBounty" + }, + "timeline": [ + { + "status": "pending_maintainer_approval", + "requires_maintainer_action": true, + "comment_type": "bounty_pending_approval" + }, + { + "status": "available", + "requires_maintainer_action": false, + "comment_type": "bounty_announcement", + "required_pr_syntax": "fixes #83" + } + ] +}