Skip to content

feat: add issue duplicate detection and contributor automation - #6

Open
akramcodez wants to merge 5 commits into
Nano-Collective:mainfrom
akramcodez:feat/first-time-contributor-onboarding
Open

akramcodez wants to merge 5 commits into
Nano-Collective:mainfrom
akramcodez:feat/first-time-contributor-onboarding

Conversation

@akramcodez

Copy link
Copy Markdown
Member

Summary

This PR adds GitHub Actions to automate issue and contributor management across Nano Collective repositories.

It introduces three related automations:

  • Duplicate issue detection - automatically identifies potential duplicate issues and applies the appropriate label with a reference to the original issue.
  • First-time contributor labeling - identifies contributions from first-time contributors so they can be recognized automatically.
  • First merged PR onboarding - after a first-time contributor's PR is merged, automatically creates a PR to add them to the organization's contributor list.

The workflows are implemented as reusable workflows in Nano-Collective/.github, with lightweight caller workflows that can be adopted by individual repositories.

Features

1. Duplicate Issue Detection

The duplicate issue workflow runs when an issue is opened or edited.

It:

  • Fetches the issue and relevant open issue history.
  • Uses Nanocoder to determine whether the issue is likely a duplicate.
  • Validates the model response before taking action.
  • Adds the duplicate label when a duplicate is identified.
  • Adds a comment referencing the original issue.
  • Uses agent:duplicate-uncertain when the result has low confidence.
  • Handles cases where no duplicate is identified without modifying the issue.

The workflow also avoids executing untrusted repository code and uses the configured model input rather than hardcoding a model.

2. First-Time Contributor Label

The contributor automation detects contributors who are making their first contribution to the organization.

This allows repositories to automatically identify first-time contributors and apply the appropriate contributor labeling without requiring manual maintainer intervention.

The detection is organization-scoped rather than simply checking whether the user has ever contributed to GitHub.

3. First Merged PR → Organization Contributor PR

When a first-time contributor's PR is merged, the workflow automatically:

  1. Resolves the PR author.
  2. Fetches their GitHub profile.
  3. Checks whether they have already contributed to the organization.
  4. Checks whether they already exist in contributors.json.
  5. Checks for an existing onboarding PR.
  6. Creates a contributor branch when necessary.
  7. Adds their contributor information to contributors.json.
  8. Opens a PR against Nano-Collective/.github.

The workflow is designed to be idempotent, so repeated contributions from the same user should not result in duplicate contributor entries or onboarding PRs.

Example contributor entry:

{
  "github": "octocat",
  "profile": {
    "login": "octocat",
    "name": "The Octocat",
    "bio": "...",
    "avatar_url": "...",
    "html_url": "...",
    "blog": "...",
    "company": "@GitHub",
    "twitter_username": "octocat"
  },
  "first_pr": {
    "repo": "Nano-Collective/nanocoder",
    "number": 123,
    "url": "...",
    "merged_at": "..."
  }
}

Implementation

The workflows are exposed as reusable workflows so individual Nano Collective repositories only need a small caller workflow.

The implementation also:

  • Uses pull_request_target for the contributor workflow to safely handle fork-based PRs.
  • Does not check out or execute code from contributor PRs.
  • Uses GitHub APIs for repository and contributor data.
  • Keeps the target repository and contributor file configurable.
  • Includes skip/idempotency checks for existing contributors and onboarding PRs.
  • Supports manual workflow dispatch for testing/backfill scenarios.
  • Keeps the duplicate issue workflow independent from contributor onboarding.

Required Setup

For the first-time contributor onboarding workflow:

  • Configure PAT_TOKEN with:

    • contents: write
    • pull-requests: write
      on the target Nano-Collective/.github repository.
  • Add contributors.json to Nano-Collective/.github with an initial value of:

[]
  • Add the caller workflow to the repositories where contributor onboarding should be enabled.
  • Ensure the relevant repository ruleset/workflow configuration permits the required pull_request_target workflow.

Testing

Static validation completed:

  • bash -n passes for all workflow run blocks.
  • YAML parsing passes for all five files.
  • Duplicate/orphaned Bash content has been removed.
  • Caller inputs match the reusable workflow inputs.
  • The existing duplicate issue workflow remains unchanged.

The recommended live test is to merge a PR from a GitHub account with no previous merged PRs in the organization and verify that the contributor onboarding PR is automatically created.

Notes

This is intended as an initial rollout of the automation. Some operational considerations remain, including GitHub search indexing latency and concurrent updates to contributors.json.

@will-lamerton

Copy link
Copy Markdown
Member

Thanks for this — the architecture is right (reusable workflow + thin caller, no checkout under pull_request_target, plan-then-write). But I don't think either workflow runs successfully as written. Both fail on their primary path, and most of the idempotency guards described above are no-ops.

Flagging the blockers rather than the full list:

duplicate-issue.yml

  1. No checkout and no GH_REPO, so bare gh calls can't resolve the repo. nc-review.yml passes --repo "$REPO" on every call; here it's missing on gh issue view "$N" (L176), both gh label create (L321-322), gh issue comment (L539/569/589), gh issue edit (L556/590), and gh issue view "$DUP" (L566). gh doesn't read GITHUB_REPOSITORY. Simplest fix is a job-level env: GH_REPO: ${{ github.repository }}.

  2. That's fatal at step 2. The failed gh issue view falls back to {}, and then:

    $ echo '{}' | jq -r '[.labels[].name] | any(. == "duplicate")'
    jq: error (at <stdin>:1): Cannot iterate over null (null)
    

    Under set -euo pipefail the job dies there.

  3. .author.is_bot // true inverts the bot check — jq's // fires on false as well as null, so every human author is classified as a bot:

    $ echo '{"author":{"is_bot":false}}' | jq -r '.author.is_bot // true'
    true
    
  4. "Skip if not worth checking" doesn't skip anything. exit 0 ends the step, not the job, and nothing downstream is gated on it. Needs to write skip=true to $GITHUB_OUTPUT with if: steps.skip.outputs.skip != 'true' on the later steps.

  5. bodies.tsv breaks on any multi-line issue body. printf '%s\t%s\n' "$NUM" "$BODY" with newlines in the body means the read loop later hits prose where it expects a number, and jq --argjson n "<prose>" exits non-zero → step fails. NDJSON (jq -c) or a null-delimited format would hold.

  6. No actions/setup-node. Nanocoder declares engines: {"node": ">=22"}; nc-review.yml pins setup-node@v4 / node-version: '22' for exactly this reason.

first-time-contributor.yml

  1. echo "ignored = true" — spaces around = — 5 times (L855, L877, L891, L900, L916). The runner splits on the first =, so the key is ignored and steps.plan.outputs.ignored is never set. Since the gate is != 'true', every skip path falls through to PR creation with an empty BRANCH_NAME/NEW_FILE. All four idempotency axes are inert. The author step gets this right (ignored=true), which is probably how it survived a read-through.

  2. echo "entry=$ENTRY" >> "$GITHUB_OUTPUT" writes a multi-line value with no delimiter. jq -n '{…}' pretty-prints, so the runner rejects the file with Unable to process file command 'output' successfully: Invalid format — the plan step fails on the success path too. jq -nc fixes it. (new_file right below does use the <<EOF form.)

  3. -f "force=true" (L1021) sends the string "true"; the refs API wants a boolean and will 422. -F force=true.

Worth fixing before rollout, not blocking

  • context-limit: '32000' with open-issues-limit: 100 full bodies — nc-review.yml uses 128000 for a comparable payload. This will overflow and autoCompact will start eating the output schema, which is the failure mode documented at length in nc-review.yml's config comment.
  • N+1 API calls: one gh issue view per corpus issue, 100 sequential round-trips per issue opened.
  • Caller and callee use the same concurrency group name, both scoped to the calling repo — I'd expect the callee to queue behind the caller waiting on it. Worth testing; renaming the callee's group makes it moot.
  • agents.config.json uses an unquoted heredoc (<<JSON) where nc-review.yml uses <<'JSON'. The \${MINIMAX_API_KEY} escape is correct, but any future $/backtick in provider-name or similarity-hint will expand. ${{ }} substitution happens first either way.
  • The model's summary is posted verbatim in a maintainer-looking comment from an untrusted issue body. disabledTools covers exfiltration, so it's comment-content only — but I'd length-cap and fence it.
  • join(", ") // "(none)" (L303) — join on an empty array returns "", which is truthy in jq, so "(none)" is unreachable.
  • templates/README.md links ../workflows/; from templates/ that's a non-existent repo-root path. Should be ../.github/workflows/.
  • All 5 files are missing a trailing newline.
  • The contributors.json write race is noted in the description — it belongs in templates/README.md under Idempotency too.

On testing

I'd push back on the Testing section: bash -n and YAML parsing are necessary but can't catch any of 1–9, since all nine are runtime behaviours.

Before re-review, could you:

  1. Fix 1–9.
  2. Prove it in one repo, not seven — point a caller at this branch rather than @main, open a throwaway issue that genuinely duplicates an existing one, and merge a PR from a fresh account. Run links in the PR.
  3. Add contributors.json ([]) to this repo in the same PR — the README already argues for committing it explicitly.

On rollout once it's green: I'd tag a v1 and have callers reference @v1 rather than @main for the first pass, pilot on nanocoder for a week (highest issue volume, so the duplicate detector's false-positive rate shows up fastest), then roll to the rest. Also needs confirming that the update-badges.yml PAT actually has contents: write + pull-requests: write on Nano-Collective/.github, and that each repo's ruleset in rulesets/ permits the pull_request_target workflow — that isn't touched here.

…kflows

Addresses the review on Nano-Collective#6. All nine blockers plus the non-blocking items.

duplicate-issue.yml:
- job-level GH_REPO so bare gh calls resolve the repository (was fatal at
  the first skip check)
- fix inverted bot check: jq's // fires on false, so every human author was
  classified as a bot; test is_bot explicitly instead
- skip step now writes skip=true to GITHUB_OUTPUT and every downstream step
  is gated on it; exit 0 alone never gated anything
- bodies.tsv -> bodies.ndjson: multi-line issue bodies broke the TSV read
  loop and crashed --argjson; NDJSON survives any body content
- guard the label-iteration jq against the {} fallback (Cannot iterate over
  null under set -euo pipefail)
- add actions/setup-node@v4 / node 22 (nanocoder declares engines >=22);
  same pin as nc-review
- context-limit 32000 -> 128000 to match nc-review; 100 full bodies
  overflowed 32000 and autoCompact ate the output schema
- quote the agents.config.json heredoc (<<'JSON', same as nc-review) and
  drop the now-redundant \$ escape
- fix join(", ") // "(none)": join on [] returns "" which is truthy in jq,
  so the fallback was unreachable
- length-cap (1000 chars) and fence the model-authored summary; it is
  derived from an untrusted issue body and was posted verbatim in a
  maintainer-looking comment
- callee concurrency group renamed duplicate-issue-callee-* so it does not
  queue behind the caller's identically-named group

first-time-contributor.yml:
- fix "ignored = true" -> "ignored=true" in five skip paths (spaces around
  = made the key "ignored " and every idempotency guard inert)
- entry= output written with heredoc delimiter form; the pretty-printed
  multi-line jq output was rejected by the runner ("Invalid format") and
  killed the success path too
- -f force=true -> -F force=true: the refs API wants a boolean, -f sends
  the string "true" and 422s
- callee concurrency group renamed first-time-contributor-callee-*

templates/README.md:
- fix ../workflows/ link from templates/ (repo-root path that does not
  resolve) to ../.github/workflows/
- document the contributors.json write race and search-index latency under
  a Known limitations section

All five files: trailing newlines added. bash -n passes on every run block,
YAML parses for all files, caller inputs verified against callee inputs.
Two remaining review items from Nano-Collective#6:

- duplicate-issue.yml: replace the per-issue REST loop (100 sequential
  round-trips per triaged issue) with one batched GraphQL call using
  aliased issue(number:) lookups. Response is filtered to NDJSON with jq
  and falls back to the per-issue REST path if the batched call fails,
  rather than triaging against an empty corpus.
- contributors.json: add the initial empty registry file to the repo root,
  as the README already argued for committing it explicitly so the first
  contributor PR is not also a "create file" PR.
@akramcodez

Copy link
Copy Markdown
Member Author

Hey @will-lamerton, addressed all the feedback and pushed the fixes. I believe it’s ready for another look whenever you get a chance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants