From 2b921282b8443c5ff2ca739404e92b58fbd0e403 Mon Sep 17 00:00:00 2001 From: Taewoon Kim Date: Thu, 5 Nov 2026 09:27:47 +0900 Subject: [PATCH] CI: port ArcadeData/arcadedb's rule-based repo automation Issue labels from rules, the in-progress label, a release milestone on merge, a PR template and issue forms, dependabot auto-merge on green for patch and minor updates, dependency review and gitignore hygiene. Everything that needs a secret, an app or a repository setting is left out (model-based classification, Claude review, merge-related-issues, REUSE lint which fails on this repository today); the issue lists them. Dependabot auto-merge polls hourly with an explicit green gate rather than enabling GitHub auto-merge: the repository has no branch protection and "Allow auto-merge" is off, so --auto would merge a clean PR before its checks finish. Co-Authored-By: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/bug_report.yml | 52 ++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 28 ++++ .github/dependency-review-config.yml | 31 +++++ .github/pull_request_template.md | 21 +++ .github/scripts/dependabot_merge.py | 121 ++++++++++++++++++ .github/workflows/assign-milestone.yml | 87 +++++++++++++ .github/workflows/classify-issue.yml | 83 ++++++++++++ .github/workflows/dependabot-auto-merge.yml | 53 ++++++++ .github/workflows/dependency-review.yml | 32 +++++ .github/workflows/gitignore-hygiene.yml | 41 ++++++ .github/workflows/label-issue-in-progress.yml | 81 ++++++++++++ CLAUDE.md | 2 + docs/modules/ROOT/pages/development/ci.adoc | 18 +++ 14 files changed, 655 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependency-review-config.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/scripts/dependabot_merge.py create mode 100644 .github/workflows/assign-milestone.yml create mode 100644 .github/workflows/classify-issue.yml create mode 100644 .github/workflows/dependabot-auto-merge.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/gitignore-hygiene.yml create mode 100644 .github/workflows/label-issue-in-progress.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..91083fbf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,52 @@ +name: Bug report +description: Something in the Python bindings (arcadedb or arcadedb-jvm) does not work +labels: ["bug"] +body: + - type: dropdown + id: package + attributes: + label: Package + description: Which distribution you installed. Both import as `arcadedb`. + options: + - arcadedb (native) + - arcadedb-jvm (JVM) + - both + - not sure + validations: + required: true + - type: input + id: version + attributes: + label: Package version + description: "`arcadedb.__version__` is enough." + placeholder: 26.11.1 + validations: + required: true + - type: input + id: platform + attributes: + label: OS, architecture and Python version + placeholder: Ubuntu 24.04 x86_64, Python 3.13 + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Include the full traceback or error text. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: A short Python script that reproduces the problem raises the chance of a quick fix a lot. + render: python + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..394417e9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Bug in the ArcadeDB engine (queries, storage, server) + url: https://github.com/ArcadeData/arcadedb/issues/new + about: A problem that also shows in the Java API or in Studio belongs in the engine repository, with a Java or SQL reproduction. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..2f504ba2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,28 @@ +name: Feature request +description: Something the Python bindings should do +labels: ["enhancement"] +body: + - type: dropdown + id: package + attributes: + label: Package + options: + - arcadedb (native) + - arcadedb-jvm (JVM) + - both + - not sure + validations: + required: true + - type: textarea + id: problem + attributes: + label: What are you trying to do? + description: The task first, then why the current API does not cover it. + validations: + required: true + - type: textarea + id: proposal + attributes: + label: What would you like to see? + validations: + required: false diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml new file mode 100644 index 00000000..fc768d38 --- /dev/null +++ b/.github/dependency-review-config.yml @@ -0,0 +1,31 @@ +# Configuration for actions/dependency-review-action, read by .github/workflows/dependency-review.yml +# (adapted from ArcadeData/arcadedb). The action's configuration is flat and drops unknown keys +# silently, so every key below is one the action understands. Inline `with:` inputs of the +# workflow would override this file, so the workflow passes only `config-file`. + +# Fail the job on a vulnerability of this severity or above. +fail-on-severity: moderate + +# Runtime dependencies only, the action's default. +fail-on-scopes: + - runtime + +# Licences CLAUDE.md forbids outright (GPL and AGPL families, SSPL). Both the current SPDX ids and +# their deprecated spellings are listed because a dependency may declare either form. +deny-licenses: + - GPL-1.0 + - GPL-1.0-only + - GPL-1.0-or-later + - GPL-2.0 + - GPL-2.0-only + - GPL-2.0-or-later + - GPL-3.0 + - GPL-3.0-only + - GPL-3.0-or-later + - AGPL-1.0 + - AGPL-1.0-only + - AGPL-1.0-or-later + - AGPL-3.0 + - AGPL-3.0-only + - AGPL-3.0-or-later + - SSPL-1.0 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..f5ae8cc2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## What does this PR do? + +A short description of the change. + +## Related issues + +`Closes #N` for the issue this fixes (the in-progress label and the release milestone follow from it). Do not use a closing keyword for an issue the change only touches. + +## Performance changes + +For a change that claims a speed or memory difference: the timed A/B result (`ab.py`, with the noise run), or say why none applies. Leave this out otherwise. + +## Docs + +Pages under `docs/` and `README.md` that describe what changed, updated in this PR, or why none needs it. + +## Checklist + +- [ ] A regression test fails on the old code and passes on the new +- [ ] The test suite that the change can affect passes +- [ ] `uvx pre-commit run --files ` passes diff --git a/.github/scripts/dependabot_merge.py b/.github/scripts/dependabot_merge.py new file mode 100644 index 00000000..266de868 --- /dev/null +++ b/.github/scripts/dependabot_merge.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Squash-merge open Dependabot pull requests that are safe to merge unattended. + +A pull request is merged only when ALL of these hold: + +* it is a single-package update whose title reads "... from X to Y" (grouped updates and + anything else Dependabot titles differently stay open for a person); +* the update is a patch or minor one: the major component of X and Y is the same; +* the list of checks is not empty and every check is a success (a skipped or neutral check is + fine; a failed, pending or missing one is not); +* GitHub reports the pull request as mergeable with merge state CLEAN. + +The merge is pinned to the head commit that was inspected, so a branch Dependabot rebased in +the meantime fails the merge instead of landing unchecked code. With --dry-run nothing is merged. +Needs the `gh` CLI with GH_TOKEN set (contents: write, pull-requests: write). +""" + +import argparse +import json +import re +import subprocess +import sys + +TITLE_RE = re.compile(r"\bfrom (\S+) to (\S+)") +VERSION_RE = re.compile(r"^[v>=~^<]*(\d+)((?:\.\d+)*)") +OK_CONCLUSIONS = {"SUCCESS", "SKIPPED", "NEUTRAL"} + + +def update_kind(title): + """Return "patch-or-minor", "major" or None when the title cannot be read.""" + found = TITLE_RE.search(title) + if not found: + return None + old, new = (VERSION_RE.match(v) for v in found.groups()) + if not old or not new: + return None + return "patch-or-minor" if old.group(1) == new.group(1) else "major" + + +def checks_green(rollup): + """True when the rollup is not empty and every entry succeeded or was skipped.""" + if not rollup: + return False + for check in rollup: + if check.get("__typename") == "StatusContext": + if check.get("state") != "SUCCESS": + return False + elif ( + check.get("status") != "COMPLETED" + or check.get("conclusion") not in OK_CONCLUSIONS + ): + return False + return True + + +def gh(*args): + return subprocess.run( + ["gh", *args], check=True, capture_output=True, text=True + ).stdout + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--dry-run", action="store_true", help="report decisions, merge nothing" + ) + args = parser.parse_args() + + fields = "number,title,headRefOid,mergeable,mergeStateStatus,statusCheckRollup" + prs = json.loads( + gh( + "pr", + "list", + "--state", + "open", + "--base", + "main", + "--author", + "app/dependabot", + "--limit", + "100", + "--json", + fields, + ) + ) + failed = False + for pr in sorted(prs, key=lambda p: p["number"]): + number, title = pr["number"], pr["title"] + kind = update_kind(title) + if kind != "patch-or-minor": + print( + f"#{number}: left open ({kind or 'not a single-package update'}): {title}" + ) + elif not checks_green(pr["statusCheckRollup"]): + print(f"#{number}: left open (checks not all green yet): {title}") + elif pr["mergeable"] != "MERGEABLE" or pr["mergeStateStatus"] != "CLEAN": + print( + f"#{number}: left open ({pr['mergeable']}, {pr['mergeStateStatus']}): {title}" + ) + elif args.dry_run: + print(f"#{number}: would merge: {title}") + else: + try: + gh( + "pr", + "merge", + str(number), + "--squash", + "--delete-branch", + "--match-head-commit", + pr["headRefOid"], + ) + print(f"#{number}: merged: {title}") + except subprocess.CalledProcessError as err: + failed = True + print(f"#{number}: merge failed: {err.stderr.strip()}", file=sys.stderr) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/assign-milestone.yml b/.github/workflows/assign-milestone.yml new file mode 100644 index 00000000..f41bc33b --- /dev/null +++ b/.github/workflows/assign-milestone.yml @@ -0,0 +1,87 @@ +# Puts a merged pull request, and the issues it closes, into the open release milestone. +# +# ArcadeData/arcadedb does this by hand: every merged pull request and closed issue sits in the +# milestone of the release it ships in (one milestone per version, with a due date). Here a +# workflow does the step that gets forgotten. The target is the open milestone with the earliest +# due date (ties: the lower number), so closing a release milestone and opening the next one is +# the only manual step. Anything that already has a milestone is left alone, and so are pull +# requests opened by Dependabot (their updates are not release content). An issue closed as "not +# planned" ships in no release and gets none. +# +# `pull_request_target` so a fork's pull request still gets a token that can edit it. Nothing from +# the pull request head is checked out or run: the job calls the GitHub API only. +name: Assign release milestone + +on: + pull_request_target: + types: [closed] + issues: + types: [closed] + +permissions: + issues: write + pull-requests: read + contents: read + +jobs: + assign: + name: Assign the open release milestone + if: >- + (github.event_name == 'pull_request_target' && + github.event.pull_request.merged == true && + github.event.pull_request.user.login != 'dependabot[bot]') || + (github.event_name == 'issues' && github.event.issue.state_reason == 'completed') + runs-on: ubuntu-latest + steps: + - name: Set the milestone + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { owner, repo } = context.repo; + + const open = await github.paginate(github.rest.issues.listMilestones, { + owner, repo, state: 'open', per_page: 100, + }); + if (open.length === 0) { + core.info('No open milestone, nothing to assign.'); + return; + } + const byDue = (a, b) => + (a.due_on ? Date.parse(a.due_on) : Infinity) - (b.due_on ? Date.parse(b.due_on) : Infinity) || + a.number - b.number; + const milestone = open.sort(byDue)[0]; + + const assign = async (number, current) => { + if (current) { + core.info(`#${number}: already in "${current.title}".`); + return; + } + await github.rest.issues.update({ owner, repo, issue_number: number, milestone: milestone.number }); + core.info(`#${number}: milestone "${milestone.title}".`); + }; + + if (context.eventName === 'issues') { + await assign(context.payload.issue.number, context.payload.issue.milestone); + return; + } + + const pr = context.payload.pull_request; + await assign(pr.number, pr.milestone); + + // The issues the pull request closes, as GitHub itself resolves them. + const result = await github.graphql( + `query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 50) { + nodes { number repository { nameWithOwner } milestone { title } } + } + } + } + }`, + { owner, repo, number: pr.number }, + ); + for (const issue of result.repository.pullRequest.closingIssuesReferences.nodes) { + if (issue.repository.nameWithOwner !== `${owner}/${repo}`) continue; + await assign(issue.number, issue.milestone); + } diff --git a/.github/workflows/classify-issue.yml b/.github/workflows/classify-issue.yml new file mode 100644 index 00000000..362febb3 --- /dev/null +++ b/.github/workflows/classify-issue.yml @@ -0,0 +1,83 @@ +# Labels a new issue from rules (adapted from ArcadeData/arcadedb's classify-issue.yml, minus +# the model call: upstream asks Claude which labels fit, this repository has no API token, so a +# keyword table does it). The package comes from the "Package" dropdown of the bug form; the topic +# labels come from words in the title and body. Only labels that already exist in the repository +# are applied, and only added, never removed, so a person's labels always win. A wrong guess is +# fixed by hand like any label. +# +# Try the rules on a real issue without touching it: run the workflow by hand (dry run by default) +# and read the log. +name: Classify issue + +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: "Issue to classify" + required: true + type: number + dry_run: + description: "If true, log the labels without applying them" + required: false + default: true + type: boolean + +permissions: + contents: read + issues: write + +jobs: + classify: + # Bots (Dependabot, release automation) open issues that need no triage. + if: github.event_name == 'workflow_dispatch' || github.event.issue.user.type == 'User' + runs-on: ubuntu-latest + steps: + - name: Choose labels from the title and body + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + ISSUE_NUMBER: ${{ github.event.issue.number || inputs.issue_number }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run != false }} + with: + script: | + const { owner, repo } = context.repo; + const { data: issue } = await github.rest.issues.get({ + owner, repo, issue_number: Number(process.env.ISSUE_NUMBER), + }); + const title = issue.title || ''; + const body = issue.body || ''; + const text = `${title}\n${body}`; + + // The value under "### Package" in an issue form's body. + const pkg = (body.match(/### Package\s+([^\n]+)/) || [])[1] || ''; + + const wanted = new Set(); + if (/arcadedb-jvm/i.test(pkg)) wanted.add('jvm'); + if (/native/i.test(pkg)) wanted.add('native'); + + // label -> pattern over title and body + const rules = { + vector: /\b(vectors?|jvector|hnsw|embeddings?|find_nearest|knn)\b/i, + performance: /\b(slow|slower|performance|latency|throughput|benchmarks?|speed)\b/i, + concurrency: /\b(threads?|concurrent|concurrency|deadlocks?|race conditions?|locks?)\b/i, + timeseries: /\btime[- ]?series\b/i, + }; + for (const [label, pattern] of Object.entries(rules)) { + if (pattern.test(text)) wanted.add(label); + } + + // Never invent a label: keep only the ones the repository already has. + const existing = new Set( + (await github.paginate(github.rest.issues.listLabelsForRepo, { owner, repo, per_page: 100 })) + .map(l => l.name), + ); + const labels = [...wanted].filter(l => existing.has(l)).sort(); + + core.info(`#${issue.number} "${title}": package="${pkg}", labels=[${labels.join(', ')}]`); + if (labels.length === 0) return; + if (process.env.DRY_RUN === 'true') { + core.info('dry run: no label applied.'); + return; + } + await github.rest.issues.addLabels({ owner, repo, issue_number: issue.number, labels }); diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..39dd57c7 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,53 @@ +# Merges Dependabot pull requests that are patch or minor updates once every check is green +# (adapted from ArcadeData/arcadedb's dependabot-auto-merge.yml). Upstream merges on one human +# approval and no green condition; this repository's rule is "all green, then merge", majors get +# a look first. +# +# It polls instead of reacting to one event because no single event means "the last check just +# went green", and `gh pr merge --auto` would need the repository setting "Allow auto-merge" plus +# required checks, neither of which is on. The rules, and why each one is there, are in +# .github/scripts/dependabot_merge.py. A merge made with the repository's GITHUB_TOKEN starts no +# push workflow on main, as in upstream. +name: Dependabot auto-merge + +on: + schedule: + - cron: "17 * * * *" + workflow_dispatch: + inputs: + dry_run: + description: "If true, log the decisions without merging anything" + required: false + default: true + type: boolean + +permissions: + contents: write + pull-requests: write + +# Two overlapping runs would try to merge the same pull request twice. +concurrency: + group: dependabot-auto-merge + cancel-in-progress: false + +jobs: + merge: + # Never on a fork: the schedule would merge pull requests on someone's copy of the repository. + if: github.repository == 'ArcadeData/arcadedb-python' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run != false }} + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Merge the Dependabot pull requests that are safe + run: | + if [ "$DRY_RUN" = "true" ]; then + python3 .github/scripts/dependabot_merge.py --dry-run + else + python3 .github/scripts/dependabot_merge.py + fi diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..b4a013d3 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,32 @@ +# Fails a pull request that adds a dependency with a known vulnerability or a forbidden licence +# (adapted from the "Dependency Review" job of ArcadeData/arcadedb's studio-security-audit.yml). +# The policy lives in .github/dependency-review-config.yml. It reads the repository's dependency +# graph, so "Dependency graph" must be on in the repository settings (the default for a public +# repository). It sees pyproject.toml and the workflow files; the jars inside the wheels come from +# the arcadedb image and are not in the graph. +name: Dependency review + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + dependency-review: + name: Dependency review + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Review the dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + config-file: ./.github/dependency-review-config.yml diff --git a/.github/workflows/gitignore-hygiene.yml b/.github/workflows/gitignore-hygiene.yml new file mode 100644 index 00000000..7a09f43e --- /dev/null +++ b/.github/workflows/gitignore-hygiene.yml @@ -0,0 +1,41 @@ +# Fails when an ignore pattern matches a tracked file (ported from ArcadeData/arcadedb, first +# check only). Such a file stays versioned only because it predates the pattern, while anything +# new next to it is silently skipped by `git add`. Fix a failure by anchoring the pattern to where +# it is meant to apply, or with a `!` negation. Do not untrack the file unless it is a generated +# artifact. +name: Gitignore hygiene + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + no-tracked-file-ignored: + name: No tracked file matches .gitignore + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Assert no tracked file is matched by an ignore pattern + run: | + # --no-index evaluates the patterns against tracked paths too. Exit status 1 means + # "nothing matched", which is the pass case, so test the output, not the status. + matched="$(git ls-files | git check-ignore --no-index --stdin || true)" + if [ -n "$matched" ]; then + echo "::error::Tracked files are matched by an ignore pattern (responsible line shown):" + printf '%s\n' "$matched" | git check-ignore --no-index --verbose --stdin || true + exit 1 + fi + echo "No tracked file is matched by any .gitignore pattern." diff --git a/.github/workflows/label-issue-in-progress.yml b/.github/workflows/label-issue-in-progress.yml new file mode 100644 index 00000000..bdf6f3cd --- /dev/null +++ b/.github/workflows/label-issue-in-progress.yml @@ -0,0 +1,81 @@ +# Marks an issue "in progress" the moment a pull request declares it will close it +# (ported from ArcadeData/arcadedb, same trigger and same logic). +# +# Only closing references count ("Closes #12"), never a bare mention. GitHub's own +# `closingIssuesReferences` answers "what does this pull request close", so the keyword list and +# the same-repository rule cannot drift from what GitHub does on merge. `edited` is a trigger +# because the reference is often added to the body after the pull request is opened. +# +# `pull_request_target` so a fork's pull request still gets a token that can label. Nothing from +# the pull request head is checked out or run here: the job reads the event payload and calls the +# GitHub API only. +name: Label referenced issue as in progress + +on: + pull_request_target: + types: [opened, reopened, edited] + +permissions: + issues: write + pull-requests: read + contents: read + +concurrency: + group: label-in-progress-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + label: + name: Add "in progress" to issues this pull request closes + runs-on: ubuntu-latest + steps: + - name: Label the issues this pull request closes + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const LABEL = 'in progress'; + const { owner, repo } = context.repo; + const number = context.payload.pull_request.number; + + const query = ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + closingIssuesReferences(first: 50) { + nodes { + number + state + repository { nameWithOwner } + labels(first: 100) { nodes { name } } + } + } + } + } + }`; + + const result = await github.graphql(query, { owner, repo, number }); + const nodes = result.repository.pullRequest.closingIssuesReferences.nodes; + if (nodes.length === 0) { + core.info('No closing issue reference found in the body.'); + return; + } + + const here = `${owner}/${repo}`; + for (const issue of nodes.sort((a, b) => a.number - b.number)) { + if (issue.repository.nameWithOwner !== here) { + core.info(`${issue.repository.nameWithOwner}#${issue.number}: other repository, skipped.`); + continue; + } + if (issue.state !== 'OPEN') { + core.info(`#${issue.number}: closed, skipped.`); + continue; + } + if (issue.labels.nodes.some(l => l.name === LABEL)) { + core.info(`#${issue.number}: already labelled "${LABEL}".`); + continue; + } + await github.rest.issues.addLabels({ + owner, repo, issue_number: issue.number, labels: [LABEL], + }); + core.info(`#${issue.number}: added "${LABEL}".`); + } diff --git a/CLAUDE.md b/CLAUDE.md index 332cf3b8..0c16dc08 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,8 @@ uvx bandit==1.9.4 -c pyproject.toml -r examples --severity-level medium --confid Each package has one test and one release workflow: `test-jvm.yml` and `release-jvm.yml` for `arcadedb-jvm`, `test-native.yml` and `release-native.yml` for `arcadedb` (the release workflows call the test workflows with `workflow_call`; the PyPI trusted publishers are keyed on the release file names and the environments `pypi-jvm` and `pypi`, so never rename them without changing PyPI first). `test-python-examples.yml` runs `examples/`. `docs.yml` builds the Antora component in `docs/` as a site of its own (`docs/antora-playbook.yml`, Antora 3.2.1, a warning fails it) and attaches it as the `docs-site` artifact; it is the only check of a docs-only pull request. Link into the main manual with `{arcadedb-docs-url}/`, never an `arcadedb::` xref (the component on its own cannot resolve it). +Repository automation (no secret, modelled on ArcadeData/arcadedb): `classify-issue.yml` (rule-based labels), `label-issue-in-progress.yml`, `assign-milestone.yml` (open milestone due first), `dependabot-auto-merge.yml` (patch and minor, all checks green; `.github/scripts/dependabot_merge.py`), `dependency-review.yml`, `gitignore-hygiene.yml`; docs page `development/ci.adoc`. A pull request states its issue with `Closes #N` (the in-progress label and the milestone follow from it). + `.github/workflows/test-jvm.yml` builds ONE wheel per platform (linux/amd64, linux/arm64, darwin/arm64, windows/amd64), tagged `py3-none-` by `setup.py` (`bdist_wheel.get_tag`; `scripts/verify_wheel_tag.py` checks it), and tests that wheel on Python 3.11–3.14; also runs nightly against `arcadedata/arcadedb:latest`. `requires-python` is `>=3.11,<3.15` because the tag no longer keeps pip off 3.15 and JPype has no 3.15 wheel; drop the cap with it. The org shares a runner cap with upstream, so the unit tests run 10 legs on every event (linux/amd64 on all four Pythons, the other three platforms on 3.11 and 3.14, the shape of test-native.yml), and the examples run all four platforms on 3.14 on a pull request (4 legs, 28 checks in all) and 16 legs on push to main, the nightly run, dispatch and the release (40 checks). `scripts/select_matrix.py` holds both lists (`PULL_REQUEST_LEGS`, keyed `jvm` and `examples`), the first job of each workflow runs it, and `tests/test_select_matrix.py` pins the full matrix; docs page `development/ci.adoc`. Keep the job names `Test arcadedb (, Python )` unchanged. Both workflows accept an `arcadedb-tag` input to pin the image. `test-python-examples.yml` runs `examples/`. A dependency-floor audit locks the dependencies and extras on a throwaway uv project with `--resolution lowest-direct` for each Python classifier and runs `uvx pip-audit` on the export, so raise lower bounds when a floor has an advisory. No tool is installed with pip anywhere in CI or the scripts: one-off tools run through `uvx tool==X`, the wheel is built with `uvx --from "build[uv]==1.6.1" pyproject-build --installer uv`, and the uv install URL is pinned (`0.12.0`). `.github/workflows/release-jvm.yml` publishes `arcadedb-jvm` on a bare-version tag (`26.11.1.dev0`, `26.11.1`, `26.11.1.post1`; no `v`, never `aN`/`bN`/`rcN`). Its first job runs `scripts/check_version.py` (the devN/postN counting rule, read-only, fails closed); `scripts/release_preflight.py ` checks everything before the tag (read-only); `scripts/after_tag_verify.sh` (`PACKAGE=native` for `arcadedb`) checks the finished release; `scripts/bump_engine.py` moves the dev-engine pins after a release. The monthly steps are the "Release day" section of `development/release.adoc` and the issue template `.github/ISSUE_TEMPLATE/release.md`. The rule is stated once, on the docs page `development/release.adoc`. diff --git a/docs/modules/ROOT/pages/development/ci.adoc b/docs/modules/ROOT/pages/development/ci.adoc index 1610a65e..360c78eb 100644 --- a/docs/modules/ROOT/pages/development/ci.adoc +++ b/docs/modules/ROOT/pages/development/ci.adoc @@ -69,6 +69,24 @@ The dev-engine bump after each release, described in xref:development/release.ad * A draft pull request skips the platform jobs. Marking it ready for review starts them. * A new push to a pull request cancels the run it replaces. A run on `main` is never cancelled. +== Repository automation + +These workflows follow ArcadeData/arcadedb and need no secret. Issues and pull requests carry labels and a release milestone (one open milestone per version, with a due date). + +[cols="2,3"] +|=== +| Workflow | What it does + +| `classify-issue.yml` | Labels a new issue from rules: the package from the bug form (`native`, `jvm`) and topic words (`vector`, `performance`, `concurrency`, `timeseries`). It only adds labels that exist. Run it by hand with the issue number to see the labels without applying them. +| `label-issue-in-progress.yml` | Adds `in progress` to the issues a pull request closes (`Closes #N` in the body). +| `assign-milestone.yml` | On merge, puts the pull request and the issues it closes into the open milestone with the earliest due date, unless they have one. Dependabot pull requests are skipped. +| `dependabot-auto-merge.yml` | Every hour, squash-merges Dependabot pull requests that are patch or minor updates, mergeable, and have a non-empty list of checks that all succeeded. Majors and grouped updates stay open. Logic in `.github/scripts/dependabot_merge.py`. +| `dependency-review.yml` | Fails a pull request that adds a dependency with a known vulnerability (moderate or worse) or a GPL, AGPL or SSPL licence. Policy in `.github/dependency-review-config.yml`. +| `gitignore-hygiene.yml` | Fails when a `.gitignore` pattern matches a tracked file. +|=== + +To close a release, close its milestone and open the next one with its due date: `assign-milestone.yml` picks the open milestone that is due first. + == Native build The workflow `test-native.yml` builds the native image of the `arcadedb` package and runs the test suite against the installed wheel. It does not follow the table above. It never publishes; `release-native.yml` calls it on a version tag.