diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..a390b1cb7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @DerDennisOP diff --git a/.github/ISSUE_TEMPLATE/bug-report-backend.yml b/.github/ISSUE_TEMPLATE/bug-report-backend.yml new file mode 100644 index 000000000..3289dc0c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-backend.yml @@ -0,0 +1,41 @@ +name: Bug report (backend) +description: A stuck or failed evaluation, build or worker, with a diagnostic report attached. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Generate the report on the evaluation panel: three-dot menu -> **Diagnostic report** -> **Generate**. See [Report a Bug with a Diagnostic Report](https://wavelens.github.io/gradient/guides/diagnostic-report/). + + - type: textarea + id: description + attributes: + label: What went wrong + description: What happened and what you expected instead. + validations: + required: true + + - type: textarea + id: report + attributes: + label: Diagnostic report + description: GitHub does not accept `.db` files. Compress the report first (`gzip gradient-report-*.db`), then drag the `.db.gz` file in here. + placeholder: Drop gradient-report--.db.gz here + validations: + required: true + + - type: input + id: version + attributes: + label: Gradient version + description: Shown in the frontend footer. + placeholder: 2.0.0 + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Server or worker logs + description: Relevant lines from `journalctl -u gradient-server` or `journalctl -u gradient-worker`. + render: text diff --git a/.github/ISSUE_TEMPLATE/bug-report-cli.yml b/.github/ISSUE_TEMPLATE/bug-report-cli.yml new file mode 100644 index 000000000..2c0ff1508 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-cli.yml @@ -0,0 +1,41 @@ +name: Bug report (CLI) +description: A failing or misbehaving `gradient` command. +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: What went wrong + description: What happened and what you expected instead. + validations: + required: true + + - type: textarea + id: command + attributes: + label: Command and output + description: The full command line and its output. Remove tokens and other secrets first. + render: shell + validations: + required: true + + - type: input + id: cli-version + attributes: + label: CLI version + description: Output of `gradient --version`. + validations: + required: true + + - type: input + id: server-version + attributes: + label: Server version + description: Shown in the frontend footer. + placeholder: 2.0.0 + + - type: input + id: system + attributes: + label: Operating system and installation + placeholder: NixOS 26.05, nix profile install github:wavelens/gradient#gradient-cli diff --git a/.github/ISSUE_TEMPLATE/bug-report-frontend.yml b/.github/ISSUE_TEMPLATE/bug-report-frontend.yml new file mode 100644 index 000000000..c96107469 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-frontend.yml @@ -0,0 +1,60 @@ +name: Bug report (frontend) +description: A broken page, wrong display or accessibility barrier in the web UI. +labels: ["bug", "frontend"] +body: + - type: textarea + id: description + attributes: + label: What went wrong + description: What happened and what you expected instead. + validations: + required: true + + - type: input + id: page + attributes: + label: Page + description: Path of the affected page, without the domain. + placeholder: /project/web/task/main/settings + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open the task page + 2. Click the three-dot menu on the evaluation panel + 3. ... + + - type: textarea + id: screenshot + attributes: + label: Screenshot or recording + description: Drag images or videos in here. + + - type: input + id: browser + attributes: + label: Browser and operating system + description: Also any assistive technology, e.g. a screen reader. + placeholder: Firefox 143, NixOS 26.05 + validations: + required: true + + - type: input + id: version + attributes: + label: Gradient version + description: Shown in the frontend footer. + placeholder: 2.0.0 + validations: + required: true + + - type: textarea + id: console + attributes: + label: Browser console errors + description: Errors from the developer tools console (F12). + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..c8bbca494 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/wavelens/gradient/security/advisories/new + about: Report vulnerabilities privately, never in a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..578ef709a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,44 @@ +name: Feature request +description: A new capability or a change to existing behavior. +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Check the [Roadmap](https://wavelens.github.io/gradient/roadmap/) first. Open questions fit better in [GitHub Discussions](https://github.com/wavelens/gradient/discussions). + + - type: dropdown + id: area + attributes: + label: Area + options: + - Server + - Worker + - Frontend + - CLI + - NixOS module + - Documentation + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem + description: The use case and the current blocker. + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed solution + description: How Gradient could solve it, from the user's side. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: Workarounds in use today, or how other CI systems solve it. diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..e6093be9f --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,18 @@ +changelog: + exclude: + authors: + - gradient-ci[bot] + - dependabot[bot] + categories: + - title: Breaking Changes + labels: [breaking] + - title: Security + labels: [security] + - title: Features + labels: [enhancement] + - title: Fixes + labels: [bug] + - title: Documentation + labels: [documentation] + - title: Other Changes + labels: ["*"] diff --git a/.github/workflows/standalone-image.yml b/.github/workflows/standalone-image.yml index 8a85af886..d32b24456 100644 --- a/.github/workflows/standalone-image.yml +++ b/.github/workflows/standalone-image.yml @@ -11,6 +11,8 @@ on: permissions: contents: read packages: write + id-token: write + attestations: write env: IMAGE: ghcr.io/wavelens/gradient-standalone @@ -33,14 +35,48 @@ jobs: extra-substituters = https://public.gradient.ci/cache/main extra-trusted-public-keys = public.gradient.ci-main:qmxRE+saUvhNa3jqaCMWje+feVU77TjABchZrPGf7A8= + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: "Build image" run: nix build -L .#packages.${{ matrix.system }}.standalone-image - name: "Push image" + id: push env: CREDS: ${{ github.actor }}:${{ secrets.GITHUB_TOKEN }} TAG: ${{ github.ref_name }}-${{ matrix.arch }} - run: nix run nixpkgs#skopeo -- copy --dest-creds "$CREDS" docker-archive:result "docker://$IMAGE:$TAG" + run: | + nix run nixpkgs#skopeo -- copy --digestfile digest --dest-creds "$CREDS" docker-archive:result "docker://$IMAGE:$TAG" + echo "digest=$(cat digest)" >> "$GITHUB_OUTPUT" + + - name: "Generate SBOM" + uses: anchore/sbom-action@v0.24.3 + with: + image: ${{ env.IMAGE }}@${{ steps.push.outputs.digest }} + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + format: spdx-json + output-file: sbom.spdx.json + upload-artifact: false + + - name: "Attest build provenance" + uses: actions/attest-build-provenance@v4.2.2 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + + - name: "Attest SBOM" + uses: actions/attest-sbom@v4.1.0 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.push.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true manifest: needs: build @@ -53,9 +89,18 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: "Publish multi-arch manifest" + id: manifest env: REF: ${{ github.ref_name }} run: | for tag in "$REF" latest; do docker buildx imagetools create -t "$IMAGE:$tag" "$IMAGE:$REF-amd64" "$IMAGE:$REF-arm64" done + echo "digest=$(docker buildx imagetools inspect "$IMAGE:$REF" --format '{{json .Manifest}}' | jq -r .digest)" >> "$GITHUB_OUTPUT" + + - name: "Attest build provenance" + uses: actions/attest-build-provenance@v4.2.2 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.manifest.outputs.digest }} + push-to-registry: true diff --git a/ACCESSIBILITY.md b/ACCESSIBILITY.md new file mode 100644 index 000000000..0d7eff0ba --- /dev/null +++ b/ACCESSIBILITY.md @@ -0,0 +1,16 @@ +# Accessibility + +Every page of the Gradient frontend should work with a keyboard and a screen reader, without relying on color or motion. + +## In Place + +- Light, dark and system theme. +- No animation of status icons, progress bars and the command palette under `prefers-reduced-motion`. +- Command palette on `/`, navigable with the arrow keys or `Ctrl+J` / `Ctrl+K`. +- Status icons carry a text label for screen readers. + +## Reporting a Barrier + +Barriers go into a [Bug report (frontend)](https://github.com/wavelens/gradient/issues/new?template=bug-report-frontend.yml), together with the assistive technology in use. + +Accessibility issues are bugs and get the same priority as functional ones. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 000000000..22a71c19c --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,36 @@ +# Governance + +Gradient is an open source project under the AGPL, stewarded by Wavelens GmbH. + +## Roles + +| Role | Rights | Duties | +|---|---|---| +| Lead maintainer | Final say on direction, releases and disputes | Roadmap, releases, security advisories | +| Maintainer | Review, merge and triage | Code review, issue triage, release testing | +| Contributor | Issues, discussions and pull requests | [Contributing guide](./CONTRIBUTING.md) and [Code of Conduct](./CODE_OF_CONDUCT.md) | + +## Maintainers + +| Name | GitHub | Role | +|---|---|---| +| Dennis Wuitz | [@DerDennisOP](https://github.com/DerDennisOP) | Lead maintainer, Wavelens GmbH | +| Sandro Jäckel | [@SuperSandro2000](https://github.com/SuperSandro2000) | Maintainer | + +## Decisions + +- Maintainers decide day-to-day changes through pull request review. +- Every pull request needs the approval of one maintainer other than the author. +- Larger changes start as an issue or discussion before any code. +- The lead maintainer can settle a disagreement among maintainers. +- Planned direction in the [Roadmap](https://wavelens.github.io/gradient/roadmap/). + +## Becoming a Maintainer + +- Sustained, high-quality contributions over a few months. +- Nomination by an existing maintainer and approval by the lead maintainer. +- Inactive maintainers can step down or move to an emeritus list after six months without activity. + +## Changes to This Document + +Changes to the governance go through a pull request with the approval of the lead maintainer. diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 000000000..0508bd3d9 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,28 @@ +# Privacy + +Gradient instances send no data to Wavelens GmbH or any third party. There is no telemetry, no usage tracking and no phone-home. + +## Self-Hosted Data + +All data remain on infrastructure of the operator. + +| Data | Location | +|---|---| +| Accounts, projects, evaluations, build logs | PostgreSQL of the instance | +| Build outputs and cache contents | Disk or S3 storage of the operator | +| Store paths on workers | Nix store of each worker | + +- The operator is the data controller under the GDPR. +- Users can delete their own account in the web UI or through `DELETE /user`. + +## Outgoing Connections + +Gradient can only reach services the operator configured. + +- Git hosts, upstream caches and S3 storage. +- Webhooks, OIDC providers and SCIM clients. +- Crash reports to Wavelens, only with `services.gradient.sentry.enable = true`. Off by default. + +## Diagnostic Reports + +A [diagnostic report](https://wavelens.github.io/gradient/guides/diagnostic-report/) can leave the instance only as an attachment a user uploaded. Identities stay anonymized without an explicit opt-in. Credentials are never part of the file. diff --git a/README.md b/README.md index f8dba8232..9fe030e32 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ All commands: [CLI Reference](https://wavelens.github.io/gradient/reference/cli/ | Build outputs | Pass through the server | Pushed from the runner to Cachix | Large outputs go from worker straight to S3 | | Server host | Needs a writable Nix store | Hosted by GitHub | Needs no Nix store, fits in a micro-VM | | Heavy builds | Static machine list with speed factors | Fixed runner sizes | Scoring system places them by predicted memory, learned from past builds | +| Remote building over SSH | Only outbound, to its own build machines | Not available | [`ssh-ng://` store](https://wavelens.github.io/gradient/guides/build-over-ssh/) for `nixos-rebuild --build-host` and `nix copy` straight to the CI workers | | Private caches | One store for the whole instance | Free plan: 5 GB, filled quickly by full closures | Per-project caches with access control, on own S3 or disk storage | | Sign-in | Local accounts, LDAP, OIDC with roles for the whole instance | GitHub accounts | [OIDC](https://wavelens.github.io/gradient/guides/sso/) with provider groups mapped to roles per project, SCIM provisioning | | Integrations | Minimal JSON API | GitHub only | REST API, webhooks, Git Integrations and MCP server | @@ -98,19 +99,20 @@ A REST API backs the web UI and the CLI: [OpenAPI spec](./docs/gradient-api.yaml ## Roadmap -| Release | Highlights | -|---|---| -| **v2.0.0** | Stability pledge, organizations, storage migrations, continuous deep GC, cluster jobs, SSH builds, corporate design | -| **v2.1.0** | Multi-node evaluations | -| **v2.2.0** | Runner workers, executing jobs outside the Nix sandbox | -| **v3.0.0** | High availability, federation between Gradient instances | - -Details in the [Roadmap](https://wavelens.github.io/gradient/roadmap/); feedback in [GitHub Discussions](https://github.com/wavelens/gradient/discussions) or on [Matrix](https://matrix.to/#/#gradient-ci:matrix.org). +Planned releases in the [Roadmap](https://wavelens.github.io/gradient/roadmap/). Feedback in [GitHub Discussions](https://github.com/wavelens/gradient/discussions) or on [Matrix](https://matrix.to/#/#gradient-ci:matrix.org). ## Contributing Contributions are welcome: see the [Contributing Guidelines](CONTRIBUTING.md) and the [Contributor Docs](https://wavelens.github.io/gradient/contributors/). +## Project + +- **[Support](SUPPORT.md)**: community help, long-term and commercial support. +- **[Security Policy](SECURITY.md)**: private vulnerability reports. +- **[Privacy](PRIVACY.md)**: no telemetry, all data self-hosted. +- **[Governance](GOVERNANCE.md)**: roles, maintainers and decisions. +- **[Accessibility](ACCESSIBILITY.md)**: web UI support and barrier reports. + ## License [AGPL-3.0-only](./LICENSE), with license notices following the [REUSE guidelines](https://reuse.software/). Developed by [Wavelens GmbH](https://wavelens.io). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..2467fab0f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +Security patches land on `main` and in the latest release. Older releases get backports only under [long-term support](./SUPPORT.md#long-term-support). + +## Reporting a Vulnerability + +- Report privately through [GitHub Security Advisories](https://github.com/wavelens/gradient/security/advisories/new), or by mail to info@wavelens.io. +- Never open a public issue or pull request for a vulnerability. +- Include the affected version, the component (server, worker, frontend, CLI, NixOS module) and steps to reproduce. +- A [diagnostic report](https://wavelens.github.io/gradient/guides/diagnostic-report/) can go into the advisory. Keep identities anonymized unless they matter for the issue. + +Maintainers confirm the report, agree on a disclosure date with the reporter and publish an advisory together with the fix. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..a7df25ce6 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,29 @@ +# Support + +## Community + +- Questions and help on [Matrix](https://matrix.to/#/#gradient-ci:matrix.org). +- Bugs and feature requests as [GitHub issues](https://github.com/wavelens/gradient/issues/new/choose). +- Vulnerabilities only through the [Security Policy](./SECURITY.md). + +## Releases + +- Version numbers follow [Semantic Versioning](https://semver.org). +- Breaking changes to the NixOS module, the API, the worker protocol or the database schema wait for a new major version. +- Every release can migrate the previous one in place. +- Fixes land on `main` and in the latest release. + +## Long-Term Support + +Older releases get long-term support on request. Wavelens can keep a chosen release line on security and critical fixes for an agreed period. + +## Commercial Support + +Wavelens GmbH can provide paid support for companies running Gradient. + +- Long-term support for a chosen release. +- Guaranteed response times. +- Help with deployment, upgrades and scaling. +- A commercial license as an alternative to the AGPL. + +Contact info@wavelens.io. diff --git a/docs/src/get-started/standalone.md b/docs/src/get-started/standalone.md index 21d2e6211..6554b6302 100644 --- a/docs/src/get-started/standalone.md +++ b/docs/src/get-started/standalone.md @@ -29,6 +29,13 @@ A complete Gradient on one machine for trying Gradient on a personal repository. A QEMU VM on the terminal, logged in as root. The disk image `gradient.qcow2` in the current directory will keep projects, the cache and every build across restarts. Stop the VM with `Ctrl-a x`. +??? note "Image Provenance" + Release images come with a signed build provenance and an SBOM. The GitHub CLI can check the provenance against the repository. + + ```sh + gh attestation verify oci://ghcr.io/wavelens/gradient-standalone:latest -R wavelens/gradient + ``` + The first boot will take a minute. PostgreSQL will initialize, and Gradient will generate its secrets. ## 2. Log In diff --git a/docs/src/guides/diagnostic-report.md b/docs/src/guides/diagnostic-report.md index 29ec09f8e..639db8e28 100644 --- a/docs/src/guides/diagnostic-report.md +++ b/docs/src/guides/diagnostic-report.md @@ -42,7 +42,13 @@ One SQLite file explaining a stuck or failed evaluation, for attaching to a [bug ## 3. Attach the Report -Open an issue at with what went wrong and attach `gradient-report--.db`. +GitHub cannot take `.db` attachments. Compress the file first. + +```sh +gzip gradient-report-*.db +``` + +Open a [Bug report (backend)](https://github.com/wavelens/gradient/issues/new?template=bug-report-backend.yml) with what went wrong and attach `gradient-report--.db.gz`. ## Verify Deployment diff --git a/docs/src/index.md b/docs/src/index.md index 65bdeaae8..cee54384c 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -53,6 +53,7 @@ | Build outputs | Pass through the server | Pushed from the runner to Cachix | Large outputs go from worker straight to S3 | | Server host | Writable Nix store required | Hosted by GitHub | No Nix store required, small enough for a micro-VM | | Heavy builds | Static machine list with speed factors | Fixed runner sizes | Scoring system placing them by predicted memory, learned from past builds | +| Remote building over SSH | Only outbound, to its own build machines | Not available | [`ssh-ng://` store](guides/build-over-ssh.md) for `nixos-rebuild --build-host` and `nix copy` straight to the CI workers | | Private caches | One store for the whole instance | Free plan: 5 GB, filled quickly by full closures | Per-project caches with access control, on own S3 or disk storage | | Sign-in | Local accounts, LDAP, OIDC with roles for the whole instance | GitHub accounts | [OIDC](guides/sso.md) with provider groups mapped to roles per project, SCIM provisioning | | Integrations | Minimal JSON API | GitHub only | REST API, webhooks, Git Integrations and MCP server | @@ -75,6 +76,14 @@ Public Key: public.gradient.ci-main:qmxRE+saUvhNa3jqaCMWje+feVU77TjABchZrPGf7A8= - NixOS options search: - Chat: [#gradient-ci:matrix.org](https://matrix.to/#/#gradient-ci:matrix.org) +## Project + +- **[Support](https://github.com/wavelens/gradient/blob/main/SUPPORT.md)**: community help, long-term and commercial support. +- **[Security Policy](https://github.com/wavelens/gradient/blob/main/SECURITY.md)**: private vulnerability reports. +- **[Privacy](https://github.com/wavelens/gradient/blob/main/PRIVACY.md)**: no telemetry, all data self-hosted. +- **[Governance](https://github.com/wavelens/gradient/blob/main/GOVERNANCE.md)**: roles, maintainers and decisions. +- **[Accessibility](https://github.com/wavelens/gradient/blob/main/ACCESSIBILITY.md)**: web UI support and barrier reports. + [Get started](get-started/quick-start.md){ .md-button .md-button--primary } [Try the public instance](https://public.gradient.ci){ .md-button }