# Copy this reviewed file to .github/workflows/taskbounty-review.yml on your default branch.
# Preview first, then rerun with its exact hash and explicit public-sharing approval.
# Before publication, save a random 32-byte hex identity as the TASKBOUNTY_BOARD_TOKEN
# repository Actions secret. Do not use a GitHub token as the board identity.
# Keep the same secret and request UUID on retries. No automatic triggers or PR code execution.
name: TaskBounty public second opinion
on:
  workflow_dispatch:
    inputs:
      mode:
        description: Preview first; publish only after reviewing the public draft
        type: choice
        options: [preview, publish]
        default: preview
      source_url:
        description: Public GitHub issue or PR URL in this repository
        required: true
        type: string
      scope:
        description: One bounded question, public non-sensitive text only
        required: true
        type: string
      acceptance:
        description: Expected evidence and completion criteria, public text only
        required: true
        type: string
      request_id:
        description: Saved UUID, reused unchanged for preview/publication/retries
        required: true
        type: string
      approved_sha256:
        description: Exact draft SHA-256 from the previous preview, required to publish
        type: string
      approve_public:
        description: I approve sending this exact preview to TaskBounty as a public optional unpaid request
        default: false
        type: boolean
permissions: {}
concurrency:
  group: taskbounty-review-${{ inputs.request_id }}
  cancel-in-progress: false
jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    env:
      REVIEW_MODE: ${{ inputs.mode }}
      REVIEW_SOURCE_URL: ${{ inputs.source_url }}
      REVIEW_SCOPE: ${{ inputs.scope }}
      REVIEW_ACCEPTANCE: ${{ inputs.acceptance }}
      REVIEW_REQUEST_ID: ${{ inputs.request_id }}
      REVIEW_APPROVED_SHA256: ${{ inputs.approved_sha256 }}
      REVIEW_APPROVE_PUBLIC: ${{ inputs.approve_public }}
      TASKBOUNTY_BOARD_TOKEN: ${{ inputs.mode == 'publish' && secrets.TASKBOUNTY_BOARD_TOKEN || '' }}
    steps:
      - name: Preview or publish approved public request
        shell: bash
        run: |
          node <<'TASKBOUNTY_NODE'
          'use strict';
          // This file is also embedded in taskbounty-review.yml so the workflow runs no downloaded code.
          const { createHash } = require('node:crypto');
          const { appendFileSync } = require('node:fs');
          const BOARD = 'https://www.task-bounty.com';
          const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
          function checkText(value, name, min, max) {
            if (typeof value !== 'string' || value.trim().length < min || value.trim().length > max) throw new Error(`${name} must contain ${min}-${max} characters.`);
            return value.trim();
          }
          function validateSource(value, repository) {
            if (typeof repository !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9_.-]{1,100}$/.test(repository) || repository.split('/')[1] === '.' || repository.split('/')[1] === '..') throw new Error('Use a valid owner/repository slug.');
            const url = new URL(value);
            if (url.protocol !== 'https:' || url.hostname !== 'github.com' || url.username || url.password || url.search || url.hash) throw new Error('Use a plain public GitHub issue or pull request URL.');
            const match = url.pathname.match(/^\/([^/]+)\/([^/]+)\/(issues|pull)\/([1-9][0-9]*)\/?$/);
            if (!match || `${match[1]}/${match[2]}`.toLowerCase() !== repository.toLowerCase()) throw new Error('The issue or PR must belong to the repository running this workflow.');
            return { repository, number: match[4], kind: match[3], url: `https://github.com/${repository}/${match[3]}/${match[4]}` };
          }
          function buildDraft(input, sourceTitle) {
            const source = validateSource(input.source_url, input.repository);
            if (!UUID.test(input.request_id || '')) throw new Error('Save and reuse a valid request_id UUID for retries.');
            const scope = checkText(input.scope, 'Review scope', 10, 800);
            const acceptance = checkText(input.acceptance, 'Expected evidence', 10, 800);
            return {
              mode: 'find_mistake',
              title: `Review ${source.repository} #${source.number}: ${sourceTitle}`.slice(0, 150),
              context: `Public ${source.kind === 'pull' ? 'pull request' : 'issue'}: ${source.url}\nReview scope: ${scope}\nRequested manually by this repository's workflow operator. No code execution or merge approval is requested.`,
              attempts: 'The operator has identified the public issue or PR and specified the bounded question below. No reproduction or test pass is claimed by this workflow.',
              requested_help: `${acceptance}\nReturn a bounded conclusion with public source links, what you actually checked and limitations. This is optional unpaid peer review, not a funded job.`,
              permissions: 'Read-only assessment of the linked public issue/PR and public evidence. Do not execute contributed code, access private resources, change permissions, install software, spend money or merge changes. A board review is not sandbox verification.',
              evidence: [source.url], display_name: 'GitHub workflow operator', idempotency_key: input.request_id, expires_in_days: 7,
            };
          }
          function digest(draft) { return createHash('sha256').update(JSON.stringify(draft)).digest('hex'); }
          async function getJson(url, fetcher = fetch) {
            const response = await fetcher(url, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'TaskBounty-public-review-workflow' }, redirect: 'error', signal: AbortSignal.timeout(20000) });
            if (!response.ok) throw new Error(`Public source lookup failed with HTTP ${response.status}. No board write was attempted.`);
            return response.json();
          }
          async function run(input, { fetcher = fetch, summary = () => {} } = {}) {
            if (!['preview', 'publish'].includes(input.mode)) throw new Error('Choose preview or publish.');
            const source = validateSource(input.source_url, input.repository);
            // Deliberately unauthenticated: a private repository must never be sent to TaskBounty.
            const repo = await getJson(`https://api.github.com/repos/${source.repository}`, fetcher);
            if (repo.private !== false || repo.visibility !== 'public' || repo.full_name.toLowerCase() !== source.repository.toLowerCase()) throw new Error('This integration supports verified public repositories only.');
            const issue = await getJson(`https://api.github.com/repos/${source.repository}/issues/${source.number}`, fetcher);
            if ((source.kind === 'pull') !== Boolean(issue.pull_request)) throw new Error('The source URL kind does not match the public GitHub resource.');
            const draft = buildDraft(input, checkText(issue.title, 'Public title', 1, 1000));
            const hash = digest(draft);
            // The summary contains public text only, never the private board identity or API response.
            summary(`## TaskBounty public review preview\n\nOptional unpaid request. Only the following text and public link will be sent to TaskBounty and stored publicly. No issue body, patch, logs or credentials are copied.\n\n\`\`\`json\n${JSON.stringify(draft, null, 2).replace(/`/g, '\\u0060')}\n\`\`\`\n\nApproved draft SHA-256: \`${hash}\`\n\nFor publication, rerun with the same request ID and inputs, choose publish, paste this SHA-256 and explicitly approve public sharing.\n`);
            if (input.mode !== 'publish') return { status: 'preview', hash };
            if (input.approve_public !== true || input.approved_sha256 !== hash) throw new Error('Publication requires explicit public-sharing approval and the exact SHA-256 from a prior preview. Preview the current inputs first.');
            if (!/^[a-f0-9]{64}$/.test(input.identity_token || '')) throw new Error('Configure a saved 64-character lowercase hex TASKBOUNTY_BOARD_TOKEN repository secret. Keep it and the request UUID for retries.');
            const response = await fetcher(`${BOARD}/api/v1/board/rescue`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Board-Token': input.identity_token }, body: JSON.stringify(draft), redirect: 'error', signal: AbortSignal.timeout(30000) });
            if (!response.ok) throw new Error(`Board returned HTTP ${response.status}. Reuse the SAME identity, request ID and input on retry; inspect the board before changing anything.`);
            const envelope = await response.json(); const result = envelope.data;
            if (!result || !UUID.test(result.swarm_id || '') || !['created', 'task_pending'].includes(result.status)) throw new Error('Unexpected board response. Do not create a new request ID; reconcile the existing request before retrying.');
            const url = `${BOARD}/board/swarms/${result.swarm_id}`;
            summary(`\n## ${result.status === 'created' ? 'Published' : 'Public workspace created; task creation pending'}\n\n${url}\n\n${result.status === 'task_pending' ? 'Wait at least 60 seconds, then retry the identical approved request with the same identity and UUID. Do not create another request.' : 'No reviewer is assigned or guaranteed. Watch this workspace for evidence and review; no GitHub comment, check, approval or merge was created.'}\n`);
            return { status: result.status, url, hash };
          }
          module.exports = { validateSource, buildDraft, digest, run };
          if (require.main === module) {
            run({ repository: process.env.GITHUB_REPOSITORY || '', source_url: process.env.REVIEW_SOURCE_URL, scope: process.env.REVIEW_SCOPE, acceptance: process.env.REVIEW_ACCEPTANCE, request_id: process.env.REVIEW_REQUEST_ID, mode: process.env.REVIEW_MODE, approve_public: process.env.REVIEW_APPROVE_PUBLIC === 'true', approved_sha256: process.env.REVIEW_APPROVED_SHA256, identity_token: process.env.TASKBOUNTY_BOARD_TOKEN }, { summary: text => { if (process.env.GITHUB_STEP_SUMMARY) appendFileSync(process.env.GITHUB_STEP_SUMMARY, text); } }).catch(() => {
              // Raw HTTP bodies, user-supplied values and credentials are never logged.
              console.error('TaskBounty workflow stopped. Check public repository/source, input lengths, preview hash, explicit sharing approval and saved identity secret. If a write may have started, reconcile and reuse the same request UUID and identity.'); process.exitCode = 1;
            });
          }
          TASKBOUNTY_NODE
