From ab0145330deb97bf8fe217e16144c357dd228e78 Mon Sep 17 00:00:00 2001 From: Johnny Miller <163300+millerjp@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:09:03 +0200 Subject: [PATCH] docs: add CONTRIBUTING, CLA, CoC, CONTRIBUTORS and CLA workflows (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 Part 2 — governance documentation and CI automation: * CONTRIBUTING.md — branching, commits, test requirements, agent-gate stack (per-commit / per-issue split), performance baseline, release discipline, security reporting. British English throughout. * CLA.md — AxonOps contributor licence agreement, adapted from the axonops/mask template with project-name substitutions. Apache Licence 2.0 wording unchanged. Signatures path points at signatures/version1/cla.json. * CODE_OF_CONDUCT.md — Contributor Covenant v2.1 with the oss@axonops.com enforcement contact. * CONTRIBUTORS.md — generated from signatures/version1/cla.json via scripts/generate-contributors.sh; byte-equality enforced by TestGovernance_ContributorsFileIsGenerated. * signatures/version1/cla.json — empty signedContributors skeleton. CLA Assistant appends to this file on first signature. * scripts/generate-contributors.sh — idempotent jq-driven regenerator; mirrors mask's script with syncmap repo refs. * .github/workflows/cla.yml — CLA Assistant wiring. Requires the CLA_ASSISTANT_PAT repo secret for branch-protection bypass. * .github/workflows/contributors.yml — triggers on signature changes, regenerates CONTRIBUTORS.md, commits back to main via github-actions[bot]. * .github/workflows/ci.yml — Markdown lint globs extended to include CONTRIBUTING / CLA / CoC / CONTRIBUTORS. * scripts/gen-llms-full.sh — source list now includes CONTRIBUTING.md; llms-full.txt regenerated. * documentation_test.go — seven new governance tests covering presence, byte-equality, CLA/CoC/contributor workflow wiring, and signatures-file schema. Completes Phase 5 of the v1.0.0 roadmap. Coverage remains 100%. --- .github/workflows/ci.yml | 4 + .github/workflows/cla.yml | 65 ++++++++++++ .github/workflows/contributors.yml | 53 ++++++++++ CLA.md | 160 +++++++++++++++++++++++++++++ CODE_OF_CONDUCT.md | 83 +++++++++++++++ CONTRIBUTING.md | 101 ++++++++++++++++++ CONTRIBUTORS.md | 10 ++ documentation_test.go | 122 ++++++++++++++++++++++ llms-full.txt | 106 +++++++++++++++++++ scripts/gen-llms-full.sh | 11 +- scripts/generate-contributors.sh | 106 +++++++++++++++++++ signatures/version1/cla.json | 3 + 12 files changed, 818 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/cla.yml create mode 100644 .github/workflows/contributors.yml create mode 100644 CLA.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 CONTRIBUTORS.md create mode 100755 scripts/generate-contributors.sh create mode 100644 signatures/version1/cla.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99e3b7b..5333840 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,6 +177,10 @@ jobs: README.md SECURITY.md CHANGELOG.md + CONTRIBUTING.md + CLA.md + CODE_OF_CONDUCT.md + CONTRIBUTORS.md bdd-strict-mode-guard: name: BDD strict mode guard diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..a6aa70a --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,65 @@ +name: "CLA Assistant" + +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +# Explicit permissions because repository-level workflow permissions +# may be read-only. The PAT below carries the actual push authority +# through branch protection; GITHUB_TOKEN is used for PR comments and +# status updates. +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + # Skip unless the trigger is either a relevant PR event or a comment + # that looks like a signature / recheck request. The action filter + # inside the step duplicates this; having it at the job level avoids + # spinning up a runner for unrelated `issue_comment` events. + if: | + github.event_name == 'pull_request_target' || + (github.event_name == 'issue_comment' && + (github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA')) + steps: + - name: CLA Assistant + uses: contributor-assistant/github-action@v2.6.1 + env: + # GITHUB_TOKEN is used for PR interactions (comments, status + # checks). The default in-built token is sufficient for those. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # PERSONAL_ACCESS_TOKEN drives the commit that records the + # signature. Using an admin-scoped PAT lets the write bypass + # branch protection on `main` (required-PR, required-signatures) + # while still producing a GitHub web-flow-signed commit. + # The secret is created manually at: + # Settings → Secrets and variables → Actions → CLA_ASSISTANT_PAT + PERSONAL_ACCESS_TOKEN: ${{ secrets.CLA_ASSISTANT_PAT }} + with: + path-to-signatures: "signatures/version1/cla.json" + path-to-document: "https://github.com/axonops/syncmap/blob/main/CLA.md" + branch: "main" + # Allowlisted accounts do not need to sign the CLA. Bots only — + # humans always go through the check so they are recorded in + # the signatures file and the public CONTRIBUTORS.md list. + allowlist: "dependabot[bot],renovate[bot],github-actions[bot]" + custom-notsigned-prcomment: | + Thank you for your contribution! Before we can merge this PR, you need to sign our [Contributor License Agreement](https://github.com/axonops/syncmap/blob/main/CLA.md). + + **To sign**, post a comment on this PR containing exactly: + + > I have read the CLA Document and I hereby sign the CLA + + The CLA is a one-time agreement that covers every future contribution you make to any AxonOps open-source project. If you have questions before signing, please open a discussion or email `oss@axonops.com`. + custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA" + custom-allsigned-prcomment: "All contributors have signed the CLA. ✅" + signed-commit-message: "chore(cla): $contributorName signed the CLA in #$pullRequestNo" + create-file-commit-message: "chore(cla): initialise signatures file" + lock-pullrequest-aftermerge: true diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 0000000..6118b19 --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,53 @@ +name: "Contributors list" + +on: + push: + branches: [main] + paths: + - "signatures/version1/cla.json" + +# CLA signatures trigger a CONTRIBUTORS.md regeneration. The commit +# that pushed the signature is skipped via [skip ci] conventions +# elsewhere, but this workflow only triggers on the signatures path +# so unrelated main-branch pushes do not spin up this runner. +permissions: + contents: write + +jobs: + regenerate: + name: Regenerate CONTRIBUTORS.md + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + # Use the admin PAT so the commit pushed at the end of this + # job bypasses branch protection on `main`. GITHUB_TOKEN can + # read but cannot write through `required_pull_request_reviews`. + # The secret is created manually at: + # Settings → Secrets and variables → Actions → CLA_ASSISTANT_PAT + token: ${{ secrets.CLA_ASSISTANT_PAT }} + fetch-depth: 0 + + - name: Install jq + run: | + # jq ships on ubuntu-latest by default, but install if absent. + command -v jq >/dev/null 2>&1 || sudo apt-get install -y jq + + - name: Regenerate CONTRIBUTORS.md + run: ./scripts/generate-contributors.sh + + - name: Commit and push if changed + run: | + set -euo pipefail + + if git diff --quiet CONTRIBUTORS.md; then + echo "CONTRIBUTORS.md unchanged — nothing to commit." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add CONTRIBUTORS.md + git commit -m "chore(contributors): regenerate CONTRIBUTORS.md" + git push origin main diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..0af459d --- /dev/null +++ b/CLA.md @@ -0,0 +1,160 @@ +# AxonOps Contributor License Agreement + +This is the Contributor License Agreement ("CLA") for the open-source +projects operated by AxonOps Limited on GitHub, including +`github.com/axonops/syncmap` and any other repository under the `axonops` +GitHub organisation that references this document. + +## Plain-English summary + +You keep the copyright to everything you contribute. By signing this +CLA once, you grant AxonOps permission to include your contributions +in any AxonOps open-source project and to relicense them in future +versions of that project, under the project's stated licence (Apache +Licence 2.0 for `syncmap`). You also confirm that the work is yours to +contribute — that it is your own, that your employer does not have a +conflicting claim, and that it does not knowingly include anyone +else's proprietary code. + +You only sign this CLA once per GitHub account. The signature covers +every AxonOps project forever, past and future. + +**Signing this CLA does NOT change your rights to use, modify, or +redistribute your own contributions however you wish.** + +If anything in the full text below is unclear, please open a discussion +on the repository before signing. We are happy to explain. + +--- + +## 1. Definitions + +- **"You"** (or **"Your"**) means the copyright owner or the legal + entity authorised by the copyright owner that is making this + Agreement with AxonOps Limited ("AxonOps"). + +- **"Contribution"** means any original work of authorship, including + any modifications or additions to an existing work, that is + intentionally submitted by You to AxonOps for inclusion in, or + documentation of, any of the open-source projects operated by + AxonOps (each a "Project"). + +- **"Submitted"** means any form of electronic, verbal, or written + communication sent to AxonOps or its representatives, including but + not limited to communication on pull requests, issues, discussions, + and email, that is intentionally submitted by You for inclusion in + a Project. + +## 2. Grant of copyright licence + +Subject to the terms and conditions of this Agreement, You hereby +grant to AxonOps Limited and to recipients of software distributed by +AxonOps a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright licence to reproduce, prepare derivative works +of, publicly display, publicly perform, sublicense, and distribute +Your Contributions and such derivative works. + +## 3. Grant of patent licence + +Subject to the terms and conditions of this Agreement, You hereby +grant to AxonOps Limited and to recipients of software distributed by +AxonOps a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent licence to make, +have made, use, offer to sell, sell, import, and otherwise transfer +the Project, where such licence applies only to those patent claims +licensable by You that are necessarily infringed by Your Contribution(s) +alone or by combination of Your Contribution(s) with the Project to +which such Contribution(s) was submitted. If any entity institutes +patent litigation against You or any other entity (including a +cross-claim or counterclaim in a lawsuit) alleging that your +Contribution, or the Project to which you have contributed, constitutes +direct or contributory patent infringement, then any patent licences +granted to that entity under this Agreement for that Contribution or +Project shall terminate as of the date such litigation is filed. + +## 4. Representations + +4.1. You represent that You are legally entitled to grant the above +licences. If Your employer(s) has rights to intellectual property +that You create that includes Your Contributions, You represent that +You have received permission to make Contributions on behalf of that +employer, that Your employer has waived such rights for Your +Contributions to AxonOps, or that Your employer has executed a +separate Corporate CLA with AxonOps. + +4.2. You represent that each of Your Contributions is Your original +creation (see Section 5 for submissions on behalf of others). + +4.3. You represent that Your Contribution submissions include +complete details of any third-party licence or other restriction +(including, but not limited to, related patents and trademarks) of +which You are personally aware and which are associated with any part +of Your Contributions. + +## 5. Third-party content + +Should You wish to submit work that is not Your original creation, +You may submit it to AxonOps separately from any Contribution, +identifying the complete details of its source and of any licence or +other restriction (including, but not limited to, related patents, +trademarks, and licence agreements) of which You are personally +aware, and conspicuously marking the work as "Submitted on behalf of +a third-party: [named here]". + +## 6. Support + +You are not expected to provide support for Your Contributions, +except to the extent You desire to provide support. You may provide +support for free, for a fee, or not at all. Unless required by +applicable law or agreed to in writing, You provide Your Contributions +on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied, including, without limitation, any +warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, +or FITNESS FOR A PARTICULAR PURPOSE. + +## 7. Notification + +You agree to notify AxonOps of any facts or circumstances of which +You become aware that would make these representations inaccurate in +any respect. + +## 8. No modification of Your rights + +Nothing in this Agreement restricts Your right to use, modify, or +redistribute Your own Contributions under any terms. AxonOps does +not acquire ownership of Your contributions — only the licences +granted above. + +## 9. Scope + +This Agreement applies to all open-source projects operated by +AxonOps Limited under the `axonops` GitHub organisation. Signing it +once covers all past and future Contributions to any such project. + +--- + +## How to sign + +Signing is done automatically by our CLA Assistant bot the first time +you open a pull request against any AxonOps open-source repository. + +1. Open your first pull request. +2. The bot will comment asking you to sign. +3. Reply to the pull request with exactly: + + > I have read the CLA Document and I hereby sign the CLA + +4. Your signature is recorded in `signatures/version1/cla.json` with your + GitHub username, the pull request reference, and a timestamp. +5. The human-readable list of everyone who has signed lives at + [`CONTRIBUTORS.md`](./CONTRIBUTORS.md) and is regenerated + automatically each time the signatures file changes. + +If you prefer to sign outside GitHub (for example, as an organisation +signing on behalf of employees), please email `oss@axonops.com` and +we will work with you to put a Corporate CLA in place. + +--- + +_This Agreement is adapted from the Apache Software Foundation +Individual Contributor License Agreement v2.2._ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8f0b1e0 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at oss@axonops.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0f4924a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing to syncmap + +Thank you for your interest in contributing to `github.com/axonops/syncmap`. This document covers the expectations for code, tests, documentation, and release discipline. + +## Contributor License Agreement + +Every contributor must sign our [Contributor License Agreement](./CLA.md) before a pull request can be merged. This is a one-time step per GitHub account and covers every future contribution you make to any AxonOps open-source project. + +The CLA Assistant bot will comment on your first pull request with the signing instructions — you reply with one sentence and you are done. The process takes under a minute. Your signature is recorded in `signatures/version1/cla.json` (the audit trail) and you appear in the auto-generated [`CONTRIBUTORS.md`](./CONTRIBUTORS.md) (the public thank-you list). + +**Why we require it.** The CLA makes it explicit that (a) you have the right to contribute the code, (b) AxonOps has the licence to distribute your contributions under the project's Apache Licence 2.0, and (c) the project is legally protected if a dispute arises about contributed code. Signing the CLA does NOT change your rights to use your own contributions for any other purpose. + +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to uphold its standards. Report unacceptable behaviour privately to `oss@axonops.com`. + +## Attribution policy + +Commit messages, PR descriptions, code comments, commit trailers, and any other artefact that lands on `main` must not reference AI-tooling product names or mark content as AI-produced. The specific token list and enforcement regex live in [`llms.txt`](./llms.txt) and the CI `attribution-guard` job. This applies whether the contribution was produced by a human, an AI assistant, or both — the tooling is irrelevant to the audit trail. + +## Your first pull request + +1. **Fork** the [repository](https://github.com/axonops/syncmap) and clone your fork. +2. Create a feature branch from `main`: `feature/` or `fix/`. +3. Make your changes. Every change that touches `syncmap.go` or the test suite must go through the agent-gate stack below. +4. Push the branch to your fork and open a PR against `axonops/syncmap:main`. +5. Sign the CLA when the bot prompts you (only needed on your first PR). +6. A maintainer reviews; the agent gates run in CI; once everything is green the PR is squash-merged. + +## Branching and commits + +- **Main branch:** `main` — always buildable, always passes CI. +- **Feature work:** `feature/` branched from `main`. +- **Bug fixes:** `fix/` branched from `main`. +- Never commit directly to `main`. +- **Conventional commits** — `feat:`, `feat!:`, `fix:`, `test:`, `docs:`, `chore:`, `refactor:`, `perf:`, `ci:`. One logical change per commit. Subject ≤ 72 characters including the `(#)` suffix. +- **Every commit references an issue.** `TODO` comments in source must carry a GitHub issue number. +- No merge commits — rebase workflow. + +## Test requirements + +- Every change runs the full quality gate: `make check`. +- Unit tests use external black-box package (`syncmap_test`) with `testify` and `goleak`. Every top-level `Test…` and every `t.Run` subtest must call `t.Parallel()`. No `time.Sleep` as synchronisation; use `sync.WaitGroup`. No `fmt.Println` / `fmt.Printf` in test code. +- Tests run under `-race` always. +- Coverage gate: **95 %** on the library package. CI fails the build below the threshold. +- Concurrency-touching changes require at least one test that exercises the concurrent path. `TestLoadOrStoreContention`, `TestSwapContention`, `TestCompareAndSwapContention`, `TestConcurrentWritersReaders`, `TestRangeDuringWrites`, `TestDeleteDuringRange` are the existing patterns — match their shape. +- **BDD is the contract.** Every new public symbol or behavioural change adds a scenario to `tests/bdd/features/syncmap.feature` and any new step definitions to `tests/bdd/steps/steps.go`. Scenarios run under godog strict mode; unimplemented or pending steps fail the build. +- **Benchmarks.** Every public method has a benchmark in `syncmap_bench_test.go`. Implementation changes that could affect performance require a regenerated `bench.txt` in the same PR; `benchstat-regression-guard` fails any time/op regression ≥ 10 % at p ≤ 0.05 or any positive allocs/op delta. + +## Performance baseline + +The committed `bench.txt` is the reference against which every PR's performance is measured. Regenerate locally with: + +```bash +make bench > current.txt # raw five-sample run +make bench-regression # benchstat diff vs committed baseline +``` + +CI runs the same comparison on `ubuntu-latest`. Shared GitHub-hosted runners exhibit ±5–15 % variance for nanosecond-scale benchmarks, which the 10 % time/op threshold absorbs. If the regression guard becomes chronically flaky, options are (a) a dedicated runner, (b) a higher time/op threshold (allocs/op stays strict since allocation counts are deterministic), or (c) making the job advisory. Open an issue before changing the policy. + +## Agent-gate stack + +Every PR flows through a fixed sequence of review agents in addition to the human reviewer. The gates are enforced by `CLAUDE.md` in the repo root and are a condition of merge. They fall into three buckets by lifecycle: + +**Before filing an issue:** + +- `issue-writer` — verifies the issue has binary, testable acceptance criteria. + +**During feature work (run as you code):** + +- `test-writer` / `test-analyst` — before and after writing tests. +- `code-reviewer` / `security-reviewer` / `performance-reviewer` — after changing source. +- `docs-writer` — after changing documentation. +- `devops` — after changing CI/CD, Makefile, GoReleaser. + +**Before every commit (non-negotiable):** + +- `go-quality` — final Go quality sweep. +- `commit-message-reviewer` — enforces conventional-commit format, issue reference, subject length, no AI attribution. + +**Before closing any issue:** + +- `issue-closer` — walks each acceptance criterion and confirms it is met. + +## Documentation + +- Every exported symbol has a godoc comment that starts with the symbol name and is at least 20 characters of real prose (the `TestDocumentation_EveryExportedSymbolHasGodoc` test enforces this). +- The README Quick Start block is compile-tested by `TestReadmeQuickStart_Compiles` — if you change the snippet, you change behaviour and the test catches the drift. +- `llms.txt` is the concise AI-assistant summary (≤ 2250 words). `llms-full.txt` is the concatenated corpus, regenerated by `scripts/gen-llms-full.sh`. Any edit to the source documents (README, `doc.go`, SECURITY, CHANGELOG, CONTRIBUTING) requires running `make llms-full` and committing the regenerated file. The `llms-full-up-to-date` CI job will catch any drift. + +## Releases + +Releases happen exclusively through the [release workflow](./.github/workflows/release.yml) triggered via `workflow_dispatch`. Never create tags locally — the `tag` job in the workflow is the only permitted tag-creation path. The release workflow runs the full quality gate first, creates an annotated tag under the `github-actions[bot]` identity, runs GoReleaser, and warms the Go module proxy so `pkg.go.dev` indexes the new version promptly. + +## Reporting security issues + +Do **not** open a public issue for a suspected vulnerability. Email `oss@axonops.com`. See [`SECURITY.md`](./SECURITY.md) for the full disclosure process and response timeline. + +## Licence + +By contributing to this project, you agree that your contributions will be licensed under the project's [Apache Licence 2.0](./LICENSE), as documented in the [CLA](./CLA.md). diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..9bf610d --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,10 @@ +# Contributors + +Thank you to everyone who has signed the [Contributor License +Agreement](./CLA.md) and contributed to `github.com/axonops/syncmap`. + +> This file is **auto-generated** from [`signatures/version1/cla.json`](./signatures/version1/cla.json) +> by `.github/workflows/contributors.yml` every time a new signature +> lands. Do not edit it by hand — edits are overwritten. + +_No contributors have signed yet. Be the first — open a pull request._ diff --git a/documentation_test.go b/documentation_test.go index 80293b0..554ab14 100644 --- a/documentation_test.go +++ b/documentation_test.go @@ -62,6 +62,7 @@ func TestLLMs_FullTxtExists_AndIncludesSpecifiedSections(t *testing.T) { "# llms.txt", "# README.md", "# Package godoc (doc.go)", + "# CONTRIBUTING.md", "# SECURITY.md", "# CHANGELOG.md", "# Full godoc reference (go doc -all)", @@ -301,6 +302,127 @@ func TestGovernance_SecurityPolicyExists(t *testing.T) { "SECURITY.md must document supported versions") } +// TestGovernance_ContributingExists asserts CONTRIBUTING.md is present +// and carries the load-bearing policy sections. +func TestGovernance_ContributingExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("CONTRIBUTING.md") + require.NoError(t, err, "CONTRIBUTING.md must exist at the repo root") + + s := string(body) + for _, section := range []string{ + "Contributor License Agreement", + "Code of Conduct", + "Attribution policy", + "Branching and commits", + "Test requirements", + "Releases", + } { + assert.Contains(t, s, section, "CONTRIBUTING.md must contain %q", section) + } +} + +// TestGovernance_CLADocumentExists asserts CLA.md is present and +// carries the legally load-bearing sections. +func TestGovernance_CLADocumentExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("CLA.md") + require.NoError(t, err, "CLA.md must exist at the repo root") + + s := string(body) + for _, section := range []string{ + "Grant of copyright licence", + "Grant of patent licence", + "Representations", + "AxonOps", + } { + assert.Contains(t, s, section, "CLA.md must contain %q", section) + } +} + +// TestGovernance_CodeOfConductExists asserts CODE_OF_CONDUCT.md is +// present, is derived from the Contributor Covenant, and carries the +// AxonOps enforcement contact. +func TestGovernance_CodeOfConductExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("CODE_OF_CONDUCT.md") + require.NoError(t, err, "CODE_OF_CONDUCT.md must exist at the repo root") + + s := string(body) + assert.Contains(t, s, "Contributor Covenant", + "CODE_OF_CONDUCT.md must be derived from the Contributor Covenant") + assert.Contains(t, s, "oss@axonops.com", + "CODE_OF_CONDUCT.md must carry the AxonOps enforcement contact") + assert.NotContains(t, s, "[INSERT CONTACT METHOD]", + "CODE_OF_CONDUCT.md must have the contact placeholder filled in") +} + +// TestGovernance_CLAWorkflowExists asserts the CLA Assistant workflow +// is present and points at the syncmap repository. +func TestGovernance_CLAWorkflowExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile(".github/workflows/cla.yml") + require.NoError(t, err, ".github/workflows/cla.yml must exist") + + s := string(body) + assert.Contains(t, s, "axonops/syncmap", + "cla.yml must reference this repository (not a mask copy-paste)") + assert.Contains(t, s, "CLA_ASSISTANT_PAT", + "cla.yml must wire the CLA_ASSISTANT_PAT secret for branch-protection bypass") + assert.NotContains(t, s, "axonops/mask", + "cla.yml must not reference axonops/mask (adapt repo-specific values)") +} + +// TestGovernance_ContributorsWorkflowExists asserts the contributors +// regeneration workflow is present. +func TestGovernance_ContributorsWorkflowExists(t *testing.T) { + t.Parallel() + body, err := os.ReadFile(".github/workflows/contributors.yml") + require.NoError(t, err, ".github/workflows/contributors.yml must exist") + + s := string(body) + assert.Contains(t, s, "signatures/version1/cla.json", + "contributors.yml must trigger on the CLA signatures file") + assert.Contains(t, s, "scripts/generate-contributors.sh", + "contributors.yml must invoke the generator script") +} + +// TestGovernance_ContributorsFileIsGenerated asserts CONTRIBUTORS.md +// matches the output of the generator script (i.e. nobody has +// hand-edited it). +func TestGovernance_ContributorsFileIsGenerated(t *testing.T) { + if _, err := exec.LookPath("jq"); err != nil { + t.Skipf("jq not on PATH: %v", err) + } + t.Parallel() + + committed, err := os.ReadFile("CONTRIBUTORS.md") + require.NoError(t, err, "CONTRIBUTORS.md must exist") + + tmpOut := filepath.Join(t.TempDir(), "CONTRIBUTORS.md") + cmd := exec.Command("./scripts/generate-contributors.sh", + "signatures/version1/cla.json", tmpOut) + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run(), "generate-contributors.sh must exit 0") + + regenerated, err := os.ReadFile(tmpOut) + require.NoError(t, err) + assert.Equal(t, string(committed), string(regenerated), + "CONTRIBUTORS.md drift — run 'scripts/generate-contributors.sh' and commit the result") +} + +// TestGovernance_SignaturesFileIsValid asserts the signatures file +// exists, is valid JSON, and carries the expected schema skeleton. +func TestGovernance_SignaturesFileIsValid(t *testing.T) { + t.Parallel() + body, err := os.ReadFile("signatures/version1/cla.json") + require.NoError(t, err, "signatures/version1/cla.json must exist") + + s := string(body) + assert.Contains(t, s, "signedContributors", + "signatures/version1/cla.json must carry the signedContributors key") +} + // TestGovernance_ChangelogHasV1Entry asserts CHANGELOG.md is present // and carries the v1.0.0 entry that documents the fork changes. func TestGovernance_ChangelogHasV1Entry(t *testing.T) { diff --git a/llms-full.txt b/llms-full.txt index 756b261..c924060 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -405,6 +405,112 @@ sync.Map and sync.Mutex. --- +# CONTRIBUTING.md + +# Contributing to syncmap + +Thank you for your interest in contributing to `github.com/axonops/syncmap`. This document covers the expectations for code, tests, documentation, and release discipline. + +## Contributor License Agreement + +Every contributor must sign our [Contributor License Agreement](./CLA.md) before a pull request can be merged. This is a one-time step per GitHub account and covers every future contribution you make to any AxonOps open-source project. + +The CLA Assistant bot will comment on your first pull request with the signing instructions — you reply with one sentence and you are done. The process takes under a minute. Your signature is recorded in `signatures/version1/cla.json` (the audit trail) and you appear in the auto-generated [`CONTRIBUTORS.md`](./CONTRIBUTORS.md) (the public thank-you list). + +**Why we require it.** The CLA makes it explicit that (a) you have the right to contribute the code, (b) AxonOps has the licence to distribute your contributions under the project's Apache Licence 2.0, and (c) the project is legally protected if a dispute arises about contributed code. Signing the CLA does NOT change your rights to use your own contributions for any other purpose. + +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md). By participating, you agree to uphold its standards. Report unacceptable behaviour privately to `oss@axonops.com`. + +## Attribution policy + +Commit messages, PR descriptions, code comments, commit trailers, and any other artefact that lands on `main` must not reference AI-tooling product names or mark content as AI-produced. The specific token list and enforcement regex live in [`llms.txt`](./llms.txt) and the CI `attribution-guard` job. This applies whether the contribution was produced by a human, an AI assistant, or both — the tooling is irrelevant to the audit trail. + +## Your first pull request + +1. **Fork** the [repository](https://github.com/axonops/syncmap) and clone your fork. +2. Create a feature branch from `main`: `feature/` or `fix/`. +3. Make your changes. Every change that touches `syncmap.go` or the test suite must go through the agent-gate stack below. +4. Push the branch to your fork and open a PR against `axonops/syncmap:main`. +5. Sign the CLA when the bot prompts you (only needed on your first PR). +6. A maintainer reviews; the agent gates run in CI; once everything is green the PR is squash-merged. + +## Branching and commits + +- **Main branch:** `main` — always buildable, always passes CI. +- **Feature work:** `feature/` branched from `main`. +- **Bug fixes:** `fix/` branched from `main`. +- Never commit directly to `main`. +- **Conventional commits** — `feat:`, `feat!:`, `fix:`, `test:`, `docs:`, `chore:`, `refactor:`, `perf:`, `ci:`. One logical change per commit. Subject ≤ 72 characters including the `(#)` suffix. +- **Every commit references an issue.** `TODO` comments in source must carry a GitHub issue number. +- No merge commits — rebase workflow. + +## Test requirements + +- Every change runs the full quality gate: `make check`. +- Unit tests use external black-box package (`syncmap_test`) with `testify` and `goleak`. Every top-level `Test…` and every `t.Run` subtest must call `t.Parallel()`. No `time.Sleep` as synchronisation; use `sync.WaitGroup`. No `fmt.Println` / `fmt.Printf` in test code. +- Tests run under `-race` always. +- Coverage gate: **95 %** on the library package. CI fails the build below the threshold. +- Concurrency-touching changes require at least one test that exercises the concurrent path. `TestLoadOrStoreContention`, `TestSwapContention`, `TestCompareAndSwapContention`, `TestConcurrentWritersReaders`, `TestRangeDuringWrites`, `TestDeleteDuringRange` are the existing patterns — match their shape. +- **BDD is the contract.** Every new public symbol or behavioural change adds a scenario to `tests/bdd/features/syncmap.feature` and any new step definitions to `tests/bdd/steps/steps.go`. Scenarios run under godog strict mode; unimplemented or pending steps fail the build. +- **Benchmarks.** Every public method has a benchmark in `syncmap_bench_test.go`. Implementation changes that could affect performance require a regenerated `bench.txt` in the same PR; `benchstat-regression-guard` fails any time/op regression ≥ 10 % at p ≤ 0.05 or any positive allocs/op delta. + +## Performance baseline + +The committed `bench.txt` is the reference against which every PR's performance is measured. Regenerate locally with: + +```bash +make bench > current.txt # raw five-sample run +make bench-regression # benchstat diff vs committed baseline +``` + +CI runs the same comparison on `ubuntu-latest`. Shared GitHub-hosted runners exhibit ±5–15 % variance for nanosecond-scale benchmarks, which the 10 % time/op threshold absorbs. If the regression guard becomes chronically flaky, options are (a) a dedicated runner, (b) a higher time/op threshold (allocs/op stays strict since allocation counts are deterministic), or (c) making the job advisory. Open an issue before changing the policy. + +## Agent-gate stack + +Every PR flows through a fixed sequence of review agents in addition to the human reviewer. The gates are enforced by `CLAUDE.md` in the repo root and are a condition of merge. They fall into three buckets by lifecycle: + +**Before filing an issue:** + +- `issue-writer` — verifies the issue has binary, testable acceptance criteria. + +**During feature work (run as you code):** + +- `test-writer` / `test-analyst` — before and after writing tests. +- `code-reviewer` / `security-reviewer` / `performance-reviewer` — after changing source. +- `docs-writer` — after changing documentation. +- `devops` — after changing CI/CD, Makefile, GoReleaser. + +**Before every commit (non-negotiable):** + +- `go-quality` — final Go quality sweep. +- `commit-message-reviewer` — enforces conventional-commit format, issue reference, subject length, no AI attribution. + +**Before closing any issue:** + +- `issue-closer` — walks each acceptance criterion and confirms it is met. + +## Documentation + +- Every exported symbol has a godoc comment that starts with the symbol name and is at least 20 characters of real prose (the `TestDocumentation_EveryExportedSymbolHasGodoc` test enforces this). +- The README Quick Start block is compile-tested by `TestReadmeQuickStart_Compiles` — if you change the snippet, you change behaviour and the test catches the drift. +- `llms.txt` is the concise AI-assistant summary (≤ 2250 words). `llms-full.txt` is the concatenated corpus, regenerated by `scripts/gen-llms-full.sh`. Any edit to the source documents (README, `doc.go`, SECURITY, CHANGELOG, CONTRIBUTING) requires running `make llms-full` and committing the regenerated file. The `llms-full-up-to-date` CI job will catch any drift. + +## Releases + +Releases happen exclusively through the [release workflow](./.github/workflows/release.yml) triggered via `workflow_dispatch`. Never create tags locally — the `tag` job in the workflow is the only permitted tag-creation path. The release workflow runs the full quality gate first, creates an annotated tag under the `github-actions[bot]` identity, runs GoReleaser, and warms the Go module proxy so `pkg.go.dev` indexes the new version promptly. + +## Reporting security issues + +Do **not** open a public issue for a suspected vulnerability. Email `oss@axonops.com`. See [`SECURITY.md`](./SECURITY.md) for the full disclosure process and response timeline. + +## Licence + +By contributing to this project, you agree that your contributions will be licensed under the project's [Apache Licence 2.0](./LICENSE), as documented in the [CLA](./CLA.md). + +--- + # SECURITY.md # Security Policy diff --git a/scripts/gen-llms-full.sh b/scripts/gen-llms-full.sh index 1dbb5d2..75febca 100755 --- a/scripts/gen-llms-full.sh +++ b/scripts/gen-llms-full.sh @@ -12,12 +12,10 @@ # 1. llms.txt # 2. README.md # 3. doc.go (package comment only) -# 4. SECURITY.md -# 5. CHANGELOG.md -# 6. go doc -all github.com/axonops/syncmap -# -# CONTRIBUTING.md, CODE_OF_CONDUCT.md, CLA.md, and CONTRIBUTORS.md are -# added to this list by issue #18 when they land. +# 4. CONTRIBUTING.md +# 5. SECURITY.md +# 6. CHANGELOG.md +# 7. go doc -all github.com/axonops/syncmap set -euo pipefail @@ -72,6 +70,7 @@ section() { section "llms.txt" "llms.txt" section "README.md" "README.md" section "Package godoc (doc.go)" "doc.go-comment" +section "CONTRIBUTING.md" "CONTRIBUTING.md" section "SECURITY.md" "SECURITY.md" section "CHANGELOG.md" "CHANGELOG.md" section "Full godoc reference (go doc -all)" "godoc" diff --git a/scripts/generate-contributors.sh b/scripts/generate-contributors.sh new file mode 100755 index 0000000..c6faee9 --- /dev/null +++ b/scripts/generate-contributors.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# generate-contributors.sh — turn signatures/version1/cla.json into a +# human-readable CONTRIBUTORS.md. +# +# The CLA Assistant workflow writes every signed CLA into the JSON +# file as an append-only list. This script regenerates the public +# list from that file, sorted deterministically so re-runs produce +# byte-identical output. +# +# Usage: +# scripts/generate-contributors.sh [signatures-path] [output-path] +# +# Defaults: +# signatures-path = signatures/version1/cla.json +# output-path = CONTRIBUTORS.md +# +# Exit codes: +# 0 success (wrote output-path) +# 2 signatures file missing or unreadable +# 3 jq not installed +# +# Called by .github/workflows/contributors.yml on pushes to main that +# touch the signatures file. + +set -euo pipefail + +signatures="${1:-signatures/version1/cla.json}" +output="${2:-CONTRIBUTORS.md}" + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required but not installed" >&2 + exit 3 +fi + +# An absent signatures file means no contributors have signed yet. +# We still emit a CONTRIBUTORS.md with the banner so the file exists +# from day one; the table is simply empty. +if [[ ! -f "$signatures" ]]; then + signatures_empty=true +else + signatures_empty=false +fi + +{ + cat <<'HEADER' +# Contributors + +Thank you to everyone who has signed the [Contributor License +Agreement](./CLA.md) and contributed to `github.com/axonops/syncmap`. + +> This file is **auto-generated** from [`signatures/version1/cla.json`](./signatures/version1/cla.json) +> by `.github/workflows/contributors.yml` every time a new signature +> lands. Do not edit it by hand — edits are overwritten. + +HEADER + + if [[ "$signatures_empty" == "true" ]]; then + echo "_No contributors have signed yet. Be the first — open a pull request._" + exit 0 + fi + + # Extract the signatures array; tolerate either shape CLA Assistant + # Lite produces (`{signedContributors: [...]}` or bare array). + entries=$(jq ' + if type == "object" and has("signedContributors") then .signedContributors + elif type == "array" then . + else [] + end' "$signatures") + + count=$(echo "$entries" | jq 'length') + if [[ "$count" -eq 0 ]]; then + echo "_No contributors have signed yet. Be the first — open a pull request._" + exit 0 + fi + + echo "## Signatories" + echo + echo "| Contributor | GitHub | Signed (UTC) | First PR |" + echo "|---|---|---|---|" + + # Sort by signed_at so the table has a stable, meaningful order + # (oldest first). A pullRequestNo of 0 means "handcrafted / bootstrap + # signature" (no real PR to link) — rendered as an em-dash rather + # than a dead link. + echo "$entries" | jq -r ' + map({ + name: (.name // .login // "unknown"), + login: (.login // "unknown"), + id: (.id // 0), + signed: (.created_at // .signed_at // ""), + pr: (.pull_request_no // .pullRequestNo // 0) + }) + | sort_by(.signed) + | .[] + | "| \(.name) | [@\(.login)](https://github.com/\(.login)) | \(.signed[:10]) | " + + (if .pr == 0 then "—" else "[#\(.pr)](https://github.com/axonops/syncmap/pull/\(.pr))" end) + + " |" + ' + + echo + echo "---" + echo + echo "_${count} contributor$( [[ "$count" -eq 1 ]] || echo s ) so far. Full signature records live in [\`signatures/version1/cla.json\`](./signatures/version1/cla.json)._" +} > "$output" + +echo "Wrote $output ($(wc -l < "$output") lines)" diff --git a/signatures/version1/cla.json b/signatures/version1/cla.json new file mode 100644 index 0000000..6d978e7 --- /dev/null +++ b/signatures/version1/cla.json @@ -0,0 +1,3 @@ +{ + "signedContributors": [] +}