diff --git a/integrations/github-action/README.md b/integrations/github-action/README.md new file mode 100644 index 000000000..fb2751329 --- /dev/null +++ b/integrations/github-action/README.md @@ -0,0 +1,85 @@ +# SolFoundry GitHub Action + +Automatically post bounties to [SolFoundry](https://solfoundry.vercel.app) when GitHub issues are labeled. + +## Quick Start + +```yaml +name: Post Bounty +on: + issues: + types: [labeled] + +jobs: + bounty: + runs-on: ubuntu-latest + steps: + - uses: SolFoundry/solfoundry/integrations/github-action@main + with: + solfoundry-api-key: ${{ secrets.SOLFOUNDRY_API_KEY }} +``` + +## Inputs + +| Input | Required | Default | Description | +|-------|----------|---------|-------------| +| `solfoundry-api-key` | ✅ | — | Your SolFoundry API key | +| `solfoundry-api-url` | ❌ | `https://solfoundry.vercel.app` | SolFoundry API base URL | +| `default-reward-amount` | ❌ | `100` | Default reward amount | +| `default-reward-token` | ❌ | `USDC` | Default reward token (`USDC` or `FNDRY`) | +| `default-tier` | ❌ | `T1` | Default bounty tier (`T1`, `T2`, `T3`) | +| `bounty-label` | ❌ | `bounty` | Label that triggers bounty creation | +| `github-token` | ❌ | `${{ github.token }}` | Token for posting comments | + +## Outputs + +| Output | Description | +|--------|-------------| +| `bounty-id` | ID of the created bounty | +| `bounty-url` | URL to the created bounty on SolFoundry | + +## Label-Based Configuration + +Override defaults using labels on the issue: + +| Label | Effect | +|-------|--------| +| `bounty` | Triggers bounty creation (default label) | +| `bounty:$200` | Sets reward to 200 (default token) | +| `bounty:500FNDRY` | Sets reward to 500 FNDRY | +| `bounty-tier:T2` | Sets bounty tier to T2 | + +### Examples + +**Fixed reward per issue:** +```yaml +- uses: SolFoundry/solfoundry/integrations/github-action@main + with: + solfoundry-api-key: ${{ secrets.SOLFOUNDRY_API_KEY }} + default-reward-amount: '250' + default-reward-token: 'FNDRY' + default-tier: 'T2' +``` + +**Per-issue reward via labels:** +```yaml +- uses: SolFoundry/solfoundry/integrations/github-action@main + with: + solfoundry-api-key: ${{ secrets.SOLFOUNDRY_API_KEY }} +``` +Then add labels like `bounty:$500` and `bounty-tier:T3` on each issue. + +## Multi-Repo Setup + +1. Create a SolFoundry API key at [solfoundry.vercel.app](https://solfoundry.vercel.app) +2. Add `SOLFOUNDRY_API_KEY` as a repository or organization secret +3. Copy the workflow file to each repo's `.github/workflows/` directory +4. Label issues with `bounty` to trigger automatic bounty creation + +## How It Works + +1. The action triggers on the `issues: labeled` event +2. It checks if the added label matches the configured `bounty-label` +3. It parses optional reward/tier overrides from other labels +4. It calls the SolFoundry API to create the bounty +5. It posts a comment on the issue with the bounty link diff --git a/integrations/github-action/action.yml b/integrations/github-action/action.yml new file mode 100644 index 000000000..4accc7790 --- /dev/null +++ b/integrations/github-action/action.yml @@ -0,0 +1,38 @@ +name: 'SolFoundry Bounty Poster' +description: 'Automatically post bounties to SolFoundry when issues are labeled' +inputs: + solfoundry-api-key: + description: 'SolFoundry API key' + required: true + solfoundry-api-url: + description: 'SolFoundry API base URL' + required: false + default: 'https://solfoundry.vercel.app' + default-reward-amount: + description: 'Default reward amount' + required: false + default: '100' + default-reward-token: + description: 'Default reward token (USDC or FNDRY)' + required: false + default: 'USDC' + default-tier: + description: 'Default bounty tier (T1, T2, T3)' + required: false + default: 'T1' + bounty-label: + description: 'Label that triggers bounty creation' + required: false + default: 'bounty' + github-token: + description: 'GitHub token for posting comments' + required: false + default: '${{ github.token }}' +outputs: + bounty-id: + description: 'Created bounty ID' + bounty-url: + description: 'URL to the created bounty' +runs: + using: 'node20' + main: 'dist/index.js' diff --git a/integrations/github-action/examples/workflow.yml b/integrations/github-action/examples/workflow.yml new file mode 100644 index 000000000..156c4b625 --- /dev/null +++ b/integrations/github-action/examples/workflow.yml @@ -0,0 +1,23 @@ +name: SolFoundry Bounty + +on: + issues: + types: [labeled] + +jobs: + post-bounty: + runs-on: ubuntu-latest + # Only run when the "bounty" label is added + if: github.event.label.name == 'bounty' + steps: + - name: Post bounty to SolFoundry + uses: SolFoundry/solfoundry/integrations/github-action@main + with: + solfoundry-api-key: ${{ secrets.SOLFOUNDRY_API_KEY }} + # Optional: customize defaults + # solfoundry-api-url: 'https://solfoundry.vercel.app' + # default-reward-amount: '100' + # default-reward-token: 'USDC' + # default-tier: 'T1' + # bounty-label: 'bounty' + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/integrations/github-action/package-lock.json b/integrations/github-action/package-lock.json new file mode 100644 index 000000000..0e47d3731 --- /dev/null +++ b/integrations/github-action/package-lock.json @@ -0,0 +1,333 @@ +{ + "name": "@solfoundry/github-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@solfoundry/github-action", + "version": "1.0.0", + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@vercel/ncc": "^0.38.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@actions/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", + "license": "MIT", + "dependencies": { + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "license": "MIT", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/github": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.1.tgz", + "integrity": "sha512-xbZVcaqD4XnQAe35qSQqskb3SqIAfRyLBrHMd/8TuL7hJSz2QtbDwnNM8zWx4zO5l2fnGtseNE3MbEvD7BxVMw==", + "license": "MIT", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.2.2", + "@octokit/plugin-rest-endpoint-methods": "^10.4.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "undici": "^5.28.5" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz", + "integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", + "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.4.1", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "24.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "13.10.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^24.2.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "license": "Apache-2.0" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "license": "ISC" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/integrations/github-action/package.json b/integrations/github-action/package.json new file mode 100644 index 000000000..2ab275fb0 --- /dev/null +++ b/integrations/github-action/package.json @@ -0,0 +1,17 @@ +{ + "name": "@solfoundry/github-action", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "build": "ncc build src/main.ts -o dist" + }, + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/github": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "@vercel/ncc": "^0.38.0" + } +} diff --git a/integrations/github-action/src/api.ts b/integrations/github-action/src/api.ts new file mode 100644 index 000000000..9d0e5c272 --- /dev/null +++ b/integrations/github-action/src/api.ts @@ -0,0 +1,42 @@ +interface BountyPayload { + title: string; + description: string; + repository: string; + issueNumber: number; + issueUrl: string; + rewardAmount: number; + rewardToken: string; + tier: string; +} + +interface BountyResponse { + id: string; + url: string; +} + +export async function createBounty( + apiUrl: string, + apiKey: string, + payload: BountyPayload +): Promise { + const response = await fetch(`${apiUrl}/api/bounties`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`SolFoundry API error (${response.status}): ${text}`); + } + + const data = (await response.json()) as Record; + const bountyData = data.bounty || data; + return { + id: bountyData.id || '', + url: bountyData.url || `${apiUrl}/bounties/${bountyData.id || ''}`, + }; +} diff --git a/integrations/github-action/src/main.ts b/integrations/github-action/src/main.ts new file mode 100644 index 000000000..0bc68ecec --- /dev/null +++ b/integrations/github-action/src/main.ts @@ -0,0 +1,149 @@ +import * as core from '@actions/core'; +import * as github from '@actions/github'; +import { createBounty } from './api'; + +function parseRewardFromLabels( + labels: { name: string }[], + defaultAmount: number, + defaultToken: string +): { amount: number; token: string } { + let amount = defaultAmount; + let token = defaultToken; + + for (const label of labels) { + const name = label.name.toLowerCase(); + + // Match patterns: "bounty:$200", "bounty:200fn dry", "bounty:100usdc", "bounty:$100usdc" + const rewardMatch = name.match(/^bounty:\$?(\d+)(fn dry|usdc)?$/i); + if (rewardMatch) { + amount = parseInt(rewardMatch[1], 10); + if (rewardMatch[2]) { + token = rewardMatch[2].toUpperCase() === 'FNDRY' ? 'FNDRY' : 'USDC'; + } + } + + // Also check "reward:$200" pattern + const rewardMatch2 = name.match(/^reward:\$?(\d+)(fn dry|usdc)?$/i); + if (rewardMatch2) { + amount = parseInt(rewardMatch2[1], 10); + if (rewardMatch2[2]) { + token = rewardMatch2[2].toUpperCase() === 'FNDRY' ? 'FNDRY' : 'USDC'; + } + } + } + + return { amount, token }; +} + +function parseTierFromLabels(labels: { name: string }[], defaultTier: string): string { + for (const label of labels) { + const name = label.name.toLowerCase(); + const tierMatch = name.match(/^bounty-tier:(t[1-3])$/i); + if (tierMatch) { + return tierMatch[1].toUpperCase(); + } + } + return defaultTier; +} + +async function run(): Promise { + try { + const apiKey = core.getInput('solfoundry-api-key', { required: true }); + const apiUrl = core.getInput('solfoundry-api-url') || 'https://solfoundry.vercel.app'; + const defaultRewardAmount = parseInt(core.getInput('default-reward-amount') || '100', 10); + const defaultRewardToken = core.getInput('default-reward-token') || 'USDC'; + const defaultTier = (core.getInput('default-tier') || 'T1').toUpperCase(); + const bountyLabel = core.getInput('bounty-label') || 'bounty'; + const token = core.getInput('github-token') || ''; + + const payload = github.context.payload; + const issue = payload.issue as { + number: number; + title?: string; + body?: string; + labels?: { name: string }[]; + html_url?: string; + } | undefined; + + if (!issue) { + core.setFailed('No issue found in context. This action must run on issue events.'); + return; + } + + const { number: issueNumber, title, body, labels, html_url: issueUrl } = issue; + const repoFullName = `${github.context.repo.owner}/${github.context.repo.repo}`; + const issueLabels = labels || []; + + // Check if the issue has the bounty label + const labelNames: string[] = issueLabels.map((l) => l.name.toLowerCase()); + if (!labelNames.includes(bountyLabel.toLowerCase())) { + core.info(`Issue #${issueNumber} does not have the "${bountyLabel}" label. Skipping.`); + return; + } + + core.info(`Processing bounty for issue #${issueNumber}: ${title}`); + + // Parse reward and tier from labels + const { amount, token: rewardToken } = parseRewardFromLabels( + issueLabels, + defaultRewardAmount, + defaultRewardToken + ); + const tier = parseTierFromLabels(issueLabels, defaultTier); + + core.info(`Reward: ${amount} ${rewardToken} | Tier: ${tier}`); + + // Create bounty via API + const bounty = await createBounty(apiUrl, apiKey, { + title: title || '', + description: body || '', + repository: repoFullName, + issueNumber, + issueUrl: issueUrl || '', + rewardAmount: amount, + rewardToken, + tier, + }); + + core.info(`Bounty created: ${bounty.id}`); + core.setOutput('bounty-id', bounty.id); + core.setOutput('bounty-url', bounty.url); + + // Post comment on the issue + if (token) { + const octokit = github.getOctokit(token); + const commentBody = [ + `## 🎯 Bounty Created`, + '', + `A bounty has been posted on **SolFoundry** for this issue!`, + '', + `| Detail | Value |`, + `|--------|-------|`, + `| **Reward** | ${amount} ${rewardToken} |`, + `| **Tier** | ${tier} |`, + `| **Bounty URL** | ${bounty.url} |`, + '', + `> _This bounty was automatically created by the [SolFoundry GitHub Action](https://github.com/SolFoundry/solfoundry/tree/main/integrations/github-action)._`, + ].join('\n'); + + await octokit.rest.issues.createComment({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: issueNumber, + body: commentBody, + }); + + core.info('Comment posted on issue.'); + } else { + core.warning('No GitHub token provided; skipping comment.'); + } + } catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } else { + core.setFailed('An unexpected error occurred'); + } + } +} + +run(); diff --git a/integrations/github-action/tsconfig.json b/integrations/github-action/tsconfig.json new file mode 100644 index 000000000..1a3c17b7d --- /dev/null +++ b/integrations/github-action/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}