Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ contact_links:
- name: API Documentation
url: https://og.codercops.com/api-docs
about: Check the API docs before reporting API issues
- name: Ask a question on Discord
url: https://discord.gg/mpGdJyjaRQ
about: Chat with the maintainers and other contributors. Issue claims still happen on GitHub.
9 changes: 9 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

Closes #

## How I Tested This

<!-- Required. What you ran or clicked through to check this works. -->

## AI Assistance

<!-- For contributor PRs. Write "none", or name the tool and what you used it for. See CONTRIBUTING.md. -->

## Type of Change

- [ ] Bug fix
Expand All @@ -25,6 +33,7 @@ Closes #
- [ ] I was assigned the linked issue
- [ ] `npm run check` passes
- [ ] `npm run test` passes
- [ ] For a bug fix: the new test fails without my change
- [ ] Tested locally with `npm run dev`
- [ ] Screenshots included for visual changes
- [ ] New templates follow the [Template Guidelines](https://github.com/codercops/ogcops/blob/dev/CONTRIBUTING.md#template-guidelines)
95 changes: 95 additions & 0 deletions .github/workflows/claim-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: Claim check

# A contributor's PR counts only for an issue that was assigned to them first
# (see CONTRIBUTING.md). This check says so on the PR itself, so nobody has to
# wait for a review to find out.
#
# It uses pull_request_target so fork PRs get an answer without a maintainer
# approving the run. That is safe here: the job never checks out or runs the
# PR's code, and its token can only read.
on:
pull_request_target:
branches: [dev]
types: [opened, edited, reopened, synchronize]

permissions:
issues: read
pull-requests: read

concurrency:
group: claim-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
assigned:
name: Linked issue is assigned to the author
# Maintainers and bots don't claim issues.
if: >-
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- name: Check the linked issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

pr=$(gh api graphql \
-f owner="${REPO%/*}" -f name="${REPO#*/}" -F pr="$PR" \
-f query='query($owner: String!, $name: String!, $pr: Int!) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
author { login }
body
closingIssuesReferences(first: 20) {
nodes { number repository { nameWithOwner } assignees(first: 20) { nodes { login } } }
}
}
}
}' \
--jq '.data.repository.pullRequest')
author=$(jq -r '.author.login // ""' <<<"$pr")

# The issues GitHub will close when this PR merges ("Closes #12" in the description).
linked=$(jq --arg repo "$REPO" '[ .closingIssuesReferences.nodes[] | {
ref: (if .repository.nameWithOwner == $repo then "#\(.number)" else "\(.repository.nameWithOwner)#\(.number)" end),
assignees: [.assignees.nodes[].login]
} ]' <<<"$pr")

# GitHub can take a moment to record those links on a new PR, so when it
# has none, read "Closes #12" from the description here.
if [ "$(jq length <<<"$linked")" -eq 0 ]; then
numbers=$(jq -r '[ (.body // "") | scan("(?i)\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s+#([0-9]+)") | .[0] ] | unique | .[]' <<<"$pr")
for number in $numbers; do
# A number can also belong to a PR, or to nothing at all. Those don't count.
if issue=$(gh api "repos/$REPO/issues/$number" --jq 'select(.pull_request == null) | {ref: "#\(.number)", assignees: [.assignees[].login]}' 2>/dev/null) && [ -n "$issue" ]; then
linked=$(jq --argjson issue "$issue" '. + [$issue]' <<<"$linked")
fi
done
fi

fail() {
echo "::error title=$1::$2"
printf '### %s\n\n%s\n' "$1" "$2" >> "$GITHUB_STEP_SUMMARY"
exit 1
}

if [ "$(jq length <<<"$linked")" -eq 0 ]; then
fail "No linked issue" "This PR isn't linked to an issue. Add \"Closes #12\" to the description, with the number of the issue that was assigned to you. If you haven't claimed one yet, comment on the issue and wait to be assigned (CONTRIBUTING.md has the rules)."
fi

# Every linked issue has to be assigned to the PR author.
missing=$(jq -r --arg author "$author" '
[ .[]
| select(all(.assignees[]; ascii_downcase != ($author | ascii_downcase)))
| .ref ]
| join(", ")' <<<"$linked")

if [ -n "$missing" ]; then
fail "Issue not assigned to you" "$missing isn't assigned to @$author. Comment on the issue and wait for a maintainer to assign it before you open a PR (CONTRIBUTING.md has the rules). Once it's assigned to you, this check passes the next time it runs: push a commit or edit the PR description."
fi

echo "Assigned to @$author: $(jq -r '[.[].ref] | join(", ")' <<<"$linked")"
106 changes: 106 additions & 0 deletions .github/workflows/description-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Description check

# Every contributor PR says how it was tested and whether AI tools helped (see
# CONTRIBUTING.md). This check says so on the PR itself while one of the two is
# missing or still empty, so nobody has to wait for a review to find out.
#
# It uses pull_request_target so fork PRs get an answer without a maintainer
# approving the run. That is safe here: the job never checks out or runs the
# PR's code, and its token can only read.
on:
pull_request_target:
branches: [dev]
types: [opened, edited, reopened, synchronize]

permissions:
pull-requests: read

concurrency:
group: description-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
sections:
name: Says how it was tested and whether AI helped
# Maintainers and bots are exempt.
if: >-
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
steps:
- name: Check the description
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

# Read the description as it is now, so a re-run sees later edits.
gh api "repos/$REPO/pulls/$PR" --jq '.body // ""' > body.md

python3 - <<'PY'
import os
import re
import sys

body = open("body.md", encoding="utf-8").read()
# The hints in the PR template are HTML comments. They aren't an answer.
body = re.sub(r"<!--.*?-->", "", body, flags=re.S)
lines = body.splitlines()

# Section name, the headings we accept for it, and what to write there.
WANTED = [
(
"How I tested this",
r"how i tested|\btest(ed|ing|s)?\b|\bverif|\bvalidat|\bqa\b",
"what you ran or clicked through to check that this works",
),
(
"AI assistance",
r"\bai[ -]?(assist|usage|use|disclos|tool|help|generated)|\b(use|usage) of ai\b|^\W*ai\W*$",
'write "none", or name the tool and what you used it for',
),
]


def is_heading(line):
"""A Markdown heading, or a bold line used as one."""
return bool(re.match(r"\s{0,3}#{1,6}\s", line) or re.fullmatch(r"\s*(\*\*|__).+(\*\*|__)\s*:?\s*", line))


def answer(pattern):
"""The text under the first matching heading, or after a "Label: answer" line. None when there is neither."""
for i, line in enumerate(lines):
if is_heading(line) and re.search(pattern, line, re.I):
below = []
for following in lines[i + 1 :]:
if is_heading(following):
break
below.append(following)
return "\n".join(below).strip()
label, colon, inline = line.partition(":")
if colon and len(label) < 60 and re.search(pattern, label, re.I) and inline.strip(" *_"):
return inline.strip(" *_")
return None


problems = []
for name, pattern, hint in WANTED:
found = answer(pattern)
if found is None:
problems.append(f'The description has no "{name}" section. Add one: {hint}.')
elif not found:
problems.append(f'The "{name}" section is empty. Fill it in: {hint}.')

if not problems:
print("Both sections are filled in.")
sys.exit(0)

tail = "Edit the PR description and this check runs again (CONTRIBUTING.md has the rules)."
for problem in problems:
print(f"::error title=PR description::{problem} {tail}")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
summary.write("### PR description\n\n" + "\n".join(f"- {p}" for p in problems) + f"\n\n{tail}\n")
sys.exit(1)
PY
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ dist/
# Cloudflare: local wrangler state and secrets
.wrangler/
.dev.vars*

# local tooling notes
CLAUDE.md
AGENTS.md
139 changes: 0 additions & 139 deletions CLAUDE.md

This file was deleted.

Loading