Compare commits

...

31 Commits

Author SHA1 Message Date
adminer b2fa629187 Удалить script/getlast.sh 2026-07-27 08:28:53 +03:00
adminer 4fa96f01f8 Удалить my/scripts/PATCH_CLODE.txt 2026-07-27 08:28:43 +03:00
adminer 45daa2149a Добавить my/patch_endpoint.go.txt 2026-07-27 08:28:27 +03:00
adminer 6a6698211f Добавить my/scripts/zfs_last_snap.sh 2026-07-27 08:27:26 +03:00
adminer f4e87b4b57 Обновить my/scripts/PATCH_CLODE.txt 2026-07-24 22:31:10 +03:00
adminer 9d25ab2fb1 Добавить script/getlast.sh 2026-07-24 22:07:28 +03:00
adminer d5f2dc6b8b Добавить PATCH_CLODE.txt 2026-07-24 17:43:18 +03:00
adminer 1428678b58 Обновить internal/endpoint/endpoint.go 2026-07-24 17:32:03 +03:00
adminer 06f7183608 Обновить internal/endpoint/endpoint.go 2026-07-23 22:09:40 +03:00
adminer de9ae54de9 revert 2cc7017d74
revert Обновить internal/endpoint/endpoint.go
2026-07-18 09:55:47 +03:00
adminer 1358894d5f Обновить internal/endpoint/endpoint.go 2026-07-17 23:06:06 +03:00
adminer 8b5a942d39 Обновить internal/endpoint/endpoint.go 2026-07-17 23:01:00 +03:00
adminer bf7749fae3 Обновить internal/endpoint/endpoint.go 2026-07-17 22:52:42 +03:00
adminer 2cc7017d74 Обновить internal/endpoint/endpoint.go 2026-07-16 09:01:38 +03:00
adminer 4611293a2e Обновить internal/endpoint/endpoint.go 2026-07-16 08:46:34 +03:00
adminer 71ef236e91 Обновить internal/endpoint/endpoint.go 2026-07-16 08:44:03 +03:00
adminer 53dd46c617 Обновить internal/endpoint/endpoint.go 2026-07-16 08:40:54 +03:00
adminer 996259b9ce Обновить internal/endpoint/endpoint.go 2026-07-16 08:38:03 +03:00
adminer a6d4d55b83 Обновить internal/endpoint/endpoint.go 2026-07-16 08:30:05 +03:00
Christian Schwarz d916c93cf9 Update supporters (#935) 2026-03-17 09:41:45 +01:00
Andrew Gunnerson 46d9be415c rpm: Use %config(noreplace) for the config file (#932)
Currently, when upgrading the RPM, if the upstream `zrepl.yml` is
changed in the update, the user's `zrepl.yml` will be renamed to
`zrepl.yml.rpmsave` and the new `zrepl.yml` is installed in its place.

This commit changes the behavior so that the user's config remains in
place and the new upstream config is installed to `zrepl.yml.rpmnew`
instead.

---

This was never a problem before because
`packaging/systemd-default-zrepl.yml` was the same since the initial
release. RPM leaves edited config files alone with the upstream config
file doesn't change. However, v0.7.0 changed the file with:
9c63736489. So with this update, RPM
detects an upstream config change during the update and moves the user's
config according to `%config`'s rules
(https://www.cl.cam.ac.uk/~jw35/docs/rpm_config.html).
`%config(noreplace)` prevents this from happening.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-27 09:13:28 +01:00
Christian Schwarz b93a06d237 docs: v0.7.0 GH release link (#929) 2026-02-15 23:42:47 +01:00
Christian Schwarz 2c780b4d4e Merge pull request #927 from zrepl/post-v0.7.0-docs-and-automation
post-v0.7.0 docs changes & release automation
2026-02-15 23:17:51 +01:00
Christian Schwarz d75fe80edb /draft-release: add artifact upload with pre-flight checks
Extends the draft-release command to automatically upload release
artifacts after creating the draft release. Adds sanity checks to
verify that artifacts have been downloaded and signed before proceeding.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 22:13:44 +00:00
Christian Schwarz 05d4563f00 docs: supporters & changelog: credit all v0.6.1..v0.7.0 contributors
Added 11 code contributors from v0.6.1..v0.7.0 to supporters.rst:
- Direct committers: @bakhtiyarneyman, @ZeyadTamimi, @findesgh,
  @Malvineous, @wxiaoguang, @lpulley, @Raupinger, @fermino, @dsh2dsh
- Co-authors: @deajan, @alorimer

Added missing changelog entries for:
- @dsh2dsh: last_n keep rule fix, root_fs fix
- @lpulley: Bountysource link removal
- @Raupinger: docs newline fix
- @fermino: apt repo snippet fix

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 22:13:44 +00:00
Christian Schwarz f81f66315a add /update-supporters: claude command to maintain contributor list
- Creates new skill to automatically update docs/supporters.rst
- Extracts contributors from git history and changelog.rst
- Supports time ranges (--since) and commit ranges (v0.6.0..v0.7.0)
- Handles new contributors and recurring contributors (moves them up)
- Records processed range in RST comment
- Integrated into release workflow in README.md

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 22:13:17 +00:00
Christian Schwarz 5ea0a48224 add claude command to create github draft release 2026-02-15 22:13:17 +00:00
Christian Schwarz efdef1e8b3 add checksum verification and signature step 2026-02-15 22:13:17 +00:00
Christian Schwarz 79118ada70 release: details on how to get right number for artifact download script 2026-02-15 22:13:17 +00:00
Christian Schwarz b1ea121d53 release: artifact download: source from circleci 2026-02-15 22:13:17 +00:00
Christian Schwarz d7ee92f213 release: improvements: README 2026-02-15 22:13:17 +00:00
12 changed files with 668 additions and 18 deletions
+9 -1
View File
@@ -7,13 +7,21 @@ import requests
import time import time
import os import os
import yaml
import argparse import argparse
from pathlib import Path from pathlib import Path
circle_token = os.environ.get('CIRCLE_TOKEN') circle_token = os.environ.get('CIRCLE_TOKEN')
if not circle_token: if not circle_token:
raise ValueError('CIRCLE_TOKEN environment variable not set') cli_yml = Path.home() / ".circleci" / "cli.yml"
if cli_yml.exists():
with open(cli_yml) as f:
data = yaml.safe_load(f)
if data:
circle_token = data.get("token")
if not circle_token:
raise ValueError('CIRCLE_TOKEN not set and no token found in ~/.circleci/cli.yml')
parser = argparse.ArgumentParser(description='Download artifacts from CircleCI') parser = argparse.ArgumentParser(description='Download artifacts from CircleCI')
parser.add_argument('build_num', type=str, help='Build number') parser.add_argument('build_num', type=str, help='Build number')
+95
View File
@@ -0,0 +1,95 @@
---
description: Generate GitHub release notes from docs/changelog.rst and create a draft release with artifacts
argument-hint: <version-tag, e.g. v0.7.0>
model: sonnet
allowed-tools: Read, Write
---
# Task
Create a draft GitHub release for zrepl version $ARGUMENTS and upload release artifacts.
## Inputs
Here is the current changelog RST source:
!`cat docs/changelog.rst`
## Instructions
### Phase 1: Pre-flight Sanity Checks
Before creating the release, verify that the previous build steps have been completed.
Run these checks using bash commands:
```bash
test -d artifacts/release && echo "✓ artifacts/release directory exists" || echo "✗ Missing artifacts/release directory - run: make download-circleci-release JOB_NUM=<num>"
test -f artifacts/release/sha512sum.txt && echo "✓ sha512sum.txt exists" || echo "✗ Missing sha512sum.txt - run: make download-circleci-release JOB_NUM=<num>"
test -f artifacts/release/sha512sum.txt.asc && echo "✓ sha512sum.txt.asc exists (signed)" || echo "✗ Missing signature - run: make verify-and-sign"
ls artifacts/release/zrepl-* >/dev/null 2>&1 && echo "✓ Release artifacts present" || echo "✗ No release artifacts found"
```
If ANY check fails (shows ✗), **STOP** and print the error messages. Do not proceed with release creation.
### Phase 2: Generate Release Notes
2. Extract the changelog section for version $ARGUMENTS from the RST source above.
3. Extract all GitHub usernames mentioned in that changelog section (look for @username patterns or other contributor attributions).
4. Write a short "Highlights" section (2-4 bullets) summarizing the most impactful user-facing changes in plain language. Do NOT include commit/issue links.
5. Write a "Contributors" thank you line for all GitHub users found in step 3. Format as: "Thanks to @user1, @user2, and @user3 for their contributions to this release!"
6. Write a "Breaking Changes" section. Convert RST formatting to plain Markdown text (no commit or issue links):
- `:commit:\`abc123\`` → just remove it or describe what it changed
- `:issue:\`123\`` → just remove it or describe the issue in plain text
- `:ref:\`display text <anchor>\`` → just use `display text` as plain text
- `:repomasterlink:\`path\`` → just mention the path without a link
- RST inline code ` ``code`` ` → markdown `` `code` ``
- RST links `` `text <url>`_ `` → just use `text` without the link
If there are no breaking changes, write "No breaking changes. vX.Y.Z-1 is interoperable with vX.Y.Z." (fill in actual versions).
7. Assemble the full release notes using the template below. **IMPORTANT**: Do NOT include a detailed changelog list. Only include what is specified in the template.
8. Write the result to `artifacts/release-notes.md`.
9. Validate all links in the release notes by checking HTTP status codes with curl:
- Extract all URLs from the markdown file
- Check each URL with `curl -sI -w "%{http_code}" -o /dev/null <url>`
- Report any broken links (non-200 status codes)
- If any links are broken, stop and notify the user before creating the release
### Phase 3: Create Draft Release and Upload Artifacts
10. Run: `gh release create $ARGUMENTS --title "$ARGUMENTS" --notes-file artifacts/release-notes.md --draft`
11. Upload all artifacts: `gh release upload $ARGUMENTS artifacts/release/*`
12. Verify upload succeeded: `gh release view $ARGUMENTS --json assets --jq '.assets | length'`
13. Print the release URL and confirm that artifacts were uploaded successfully.
## Template
```
The full changelog is [on the docs site](https://zrepl.github.io/changelog.html).
## Highlights
{2-4 bullet plain-language summary of the most impactful changes}
{Thank all GitHub users mentioned in the changelog, e.g., "Thanks to @user1, @user2, and @user3 for their contributions to this release!"}
## Breaking Changes
{breaking changes, or "No breaking changes."}
## New Users
We provide [quick-start guides](https://zrepl.github.io/quickstart.html) for different usage scenarios.
We also recommend studying the [overview section of the configuration chapter](https://zrepl.github.io/configuration/overview.html).
## Testing & Upgrading
* Read the [Changelog](https://zrepl.github.io/changelog.html)
* [Run the platform tests](https://zrepl.github.io/usage.html#platform-tests) on a test system.
* Download & deploy the `zrepl` binary / distro package.
## Donations
zrepl is a spare-time project primarily developed by [Christian Schwarz](https://cschwarz.com).
Express your support through a donation to keep maintenance and feature development going.
[![Support me on Patreon](https://img.shields.io/badge/dynamic/json?color=yellow&label=Patreon&query=data.attributes.patron_count&suffix=%20patrons&url=https%3A%2F%2Fwww.patreon.com%2Fapi%2Fcampaigns%2F3095079)](https://patreon.com/zrepl) [![Donate via GitHub Sponsors](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=yellow)](https://github.com/sponsors/problame) [![Donate via Liberapay](https://img.shields.io/liberapay/patrons/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate) [![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
```
+149
View File
@@ -0,0 +1,149 @@
---
description: Update supporters list from git history and changelog contributors
argument-hint: <time-range-or-release>
model: sonnet
---
# Task
Automatically update `docs/supporters.rst` by extracting GitHub contributors from:
1. Git commit history (code contributors)
2. `docs/changelog.rst` (all mentioned GitHub users)
## Arguments
Specify a time range or release range to filter contributors:
**Time range examples:**
- `--since="2024-01-01"` - Contributors since a specific date
- `--since="1 year ago"` - Contributors in the last year
- `--since="6 months ago"` - Contributors in the last 6 months
**Release range examples:**
- `v0.6.0..HEAD` - Contributors between v0.6.0 and current HEAD
- `v0.6.0..v0.7.0` - Contributors between two releases
- `--all` - All contributors (default if no argument provided)
## Instructions
### Step 1: Extract Contributors from Git History
Parse the arguments to determine the git log range:
- If `--since="..."` is provided, use: `git log --since="..." --format='%aN <%aE>' | sort -u`
- If a commit range like `v0.6.0..HEAD` is provided, use: `git log v0.6.0..HEAD --format='%aN <%aE>' | sort -u`
- If `--all` or no argument, use: `git log --format='%aN <%aE>' | sort -u`
Parse the output to identify:
- GitHub users (look for @users.noreply.github.com emails or known GitHub patterns)
- Real names of code contributors
- Filter out the main maintainer (Christian Schwarz / problame) as they're already credited
### Step 2: Extract GitHub Users from Changelog
Read `docs/changelog.rst` and extract all GitHub usernames mentioned:
- Pattern: `` `@username <https://github.com/username>`_ ``
- Pattern: `@username` in text
- Collect all unique usernames
### Step 3: Categorize Contributors
For each contributor, determine their tier:
**Code contributors** (`|supporter-code|`):
- Anyone who appears in git history as a commit author
- Prioritize those with substantial contributions (multiple commits)
**Other contributors** (keep existing `|supporter-gold|` and `|supporter-std|`):
- Keep all existing gold/std supporters as-is (monetary supporters)
- Only update code contributors
### Step 4: Read Current Supporters File
Read `docs/supporters.rst` and parse existing entries to:
- Preserve all `|supporter-gold|` entries (monetary supporters - don't touch these)
- Preserve all `|supporter-std|` entries (monetary supporters - don't touch these)
- Preserve ALL existing `|supporter-code|` entries in their current order
- Extract list of existing code contributor names/GitHub usernames to avoid duplicates
### Step 5: Identify Contributors to Promote/Add
**IMPORTANT: Find contributors in the range, and either ADD them (if new) or MOVE them up (if recurring).**
For each GitHub user found in changelog or git history for the specified range:
1. Check if they're already listed as `|supporter-code|` in the existing file
2. Categorize them:
- **NEW**: Not currently in the file → will be added
- **RECURRING**: Already in the file with new contributions in this range → will be moved to top
- **UNCHANGED**: Not in this range → stays in original position
3. For both NEW and RECURRING contributors:
- Get their latest contribution date from the range: `git log <range> --author="email" --format="%ai" -1`
- Format as: `* |supporter-code| `Name <https://github.com/username>`_`
- Use their real name if available from git history, otherwise use GitHub username
4. Sort NEW + RECURRING contributors together by their contribution date (newest first)
5. Keep list of UNCHANGED contributors in their original order
**The final order will be: [NEW + RECURRING from range, sorted newest first] + [UNCHANGED in original order]**
### Step 6: Generate Updated File
Create the updated supporters list:
- Keep all `|supporter-gold|` entries unchanged and in their exact positions
- Keep all `|supporter-std|` entries unchanged and in their exact positions
- For `|supporter-code|` entries, reorganize as follows:
1. **Top section**: Contributors from the analyzed range (NEW + RECURRING), sorted newest first
2. **Bottom section**: UNCHANGED contributors (not in this range), in their original order
- Update the spacer comment showing the range that was just processed:
```rst
..
↓ claude --permission-mode default /update-supporters <actual-range-used>
```
Replace `<actual-range-used>` with the actual argument passed (e.g., `v0.6.1..v0.7.0`)
This comment should appear immediately before the first code contributor entry
If there's an existing comment in this format, replace it; otherwise add it
- Maintain all other file structure and RST formatting, including the "Before /update-supporters" marker comment
### Step 7: Apply Changes
- Use Edit tool to update the `docs/supporters.rst` file
- Display summary of changes made:
- Time/release range analyzed
- Number of contributors found
- New code contributors added
- Final counts by tier
## Implementation Notes
- **DO NOT modify, remove, or re-sort** any `|supporter-gold|` or `|supporter-std|` entries
- **For code contributors**: ADD new ones and MOVE UP recurring ones (those with contributions in the analyzed range)
- When extracting from git history, focus on substantial contributors (filter out single-commit or trivial changes if needed)
- **CRITICAL:** Contributors from the analyzed range go to the top
- Use `git log <range> --author="<email>" --format="%ai" -1` to get latest contribution date in the range
- Both NEW and RECURRING contributors from this range are sorted together (newest first) and placed at the TOP
- UNCHANGED contributors (not in this range) stay in their original relative order below
- This creates a chronological history: most recent release's contributors appear first
- Add/update the spacer comment showing the processed range
- Handle edge cases:
- Contributors mentioned in changelog but not in git history (external contributors, reporters)
- Contributors in git history but not mentioned in changelog (decide whether to include)
## Example Output
```
Analyzing contributors for range: v0.6.0..v0.7.0
- Found 8 code contributors in git history
- Found 12 GitHub users mentioned in changelog.rst (v0.7.0 section)
- Current supporters.rst has 8 gold, 12 std, 10 code contributors
New contributors:
+ Jane Doe (@janedoe)
+ John Smith (@jsmith)
Promoted (recurring) contributors:
↑ Alice Developer (@alice) - new contributions in v0.6.0..v0.7.0
Updated docs/supporters.rst:
- Gold supporters: 8 (unchanged)
- Std supporters: 12 (unchanged)
- Code contributors: 10 → 12
- Promoted to top: 3 contributors (2 new + 1 recurring)
```
+4 -3
View File
@@ -56,7 +56,7 @@ release-docker-mkcachemount:
endif endif
##################### PRODUCING A RELEASE ############# ##################### PRODUCING A RELEASE #############
.PHONY: release wrapup-and-checksum check-git-clean sign clean ensure-release-toolchain .PHONY: release wrapup-and-checksum check-git-clean verify-and-sign clean ensure-release-toolchain
ensure-release-toolchain: ensure-release-toolchain:
# ensure the toolchain is actually the one we expect # ensure the toolchain is actually the one we expect
@@ -211,7 +211,8 @@ tag-release:
test -n "$(ZREPL_TAG_VERSION)" || exit 1 test -n "$(ZREPL_TAG_VERSION)" || exit 1
git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)" git tag -u '328A6627FA98061D!' -m "$(ZREPL_TAG_VERSION)" "$(ZREPL_TAG_VERSION)"
sign: verify-and-sign:
cd "$(ARTIFACTDIR)/release" && sha512sum -c sha512sum.txt
gpg -u '328A6627FA98061D!' \ gpg -u '328A6627FA98061D!' \
--armor \ --armor \
--detach-sign $(ARTIFACTDIR)/release/sha512sum.txt --detach-sign $(ARTIFACTDIR)/release/sha512sum.txt
@@ -222,7 +223,7 @@ clean: docs-clean
download-circleci-release: download-circleci-release:
rm -rf "$(ARTIFACTDIR)" rm -rf "$(ARTIFACTDIR)"
mkdir -p "$(ARTIFACTDIR)/release" mkdir -p "$(ARTIFACTDIR)/release"
python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(BUILD_NUM)" "$(ARTIFACTDIR)/release" python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(JOB_NUM)" "$(ARTIFACTDIR)/release"
##################### MULTI-ARCH HELPERS ##################### ##################### MULTI-ARCH HELPERS #####################
+36 -11
View File
@@ -93,25 +93,50 @@ The procedure to issue a release is as follows:
* Prepare the release (as a PR to `master`): * Prepare the release (as a PR to `master`):
* Finalize `docs/changelog.rst` for the release. * Finalize `docs/changelog.rst` for the release.
* Update the supporters list based on contributors for this release:
```bash
claude --permission-mode default '/update-supporters v0.6.1..v0.7.0'
```
Replace version tags with the appropriate range for your release.
* Merge the PR. Docs are auto-published to zrepl.github.io on merge. * Merge the PR. Docs are auto-published to zrepl.github.io on merge.
* Tag the release: * Tag the release:
* Git tag the release on the `master` branch (e.g., `vMAJOR.MINOR.0`). * Git tag the release on the `master` branch (e.g., `vMAJOR.MINOR.0`).
```
make tag-release ZREPL_TAG_VERSION=v0.7.0
```
* Push the tag. * Push the tag.
* Build and publish: * Build and publish:
* Run the `release` pipeline (trigger via CircleCI UI). * Run the `release` pipeline against the `master` branch (trigger via CircleCI UI).
* Download artifacts: `make download-circleci-release BUILD_NUM=<circleci-build-number>` This URL: https://app.circleci.com/pipelines/github/zrepl/zrepl?branch=master.
* Create GitHub release and upload artifacts: Example pipeline: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547
```bash * Download artifacts using this handy makefile target.
gh release create vX.Y.Z --title "vX.Y.Z" --notes "See changelog" --draft Note: `JOB_NUM` must be the **job number** from the `release-upload` job, **not the pipeline number**.
gh release upload vX.Y.Z artifacts/release/* Find it via: pipeline → `release` workflow → `release-upload` job number.
Example URL: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547/workflows/65feb2c9-15d7-46ab-a551-46d62a5769b0/jobs/66079/steps
``` ```
* Review the draft release, edit the changelog, then publish. make download-circleci-release JOB_NUM=66079
```
* Verify checksums and sign the checksums file:
```
make verify-and-sign
```
* Create GitHub draft release and upload artifacts:
```bash
claude --permission-mode default '/draft-release v0.7.0'
```
This command will verify that artifacts are ready, create the draft release, and upload all artifacts.
* Review the draft release on GitHub, then publish,
with the box checked to create a GitHub "Discussion" for the release.
* Immediately after hitting publish.
* Update the release on GitHub to link to the discussion.
* Update `docs/changelog.rst` to link to the GitHub release.
* Update `docs/_templates/versions.html` to link to the GitHub release.
* Announce release in the zrepl Matrix channel & other socials as applicable.
Link to the GitHub release as the entrypoint.
* After a couple of days
* Add the .rpm and .deb files to the official zrepl repos. * Add the .rpm and .deb files to the official zrepl repos.
* Code for management of these repos: https://github.com/zrepl/package-repo-ops (private repo at this time) * Code for management of these repos: https://github.com/zrepl/package-repo-ops (private repo at this time)
* Update docs version list:
* Update `docs/_templates/versions.html` with the new release.
* Verify the link to `zrepl-noarch.tar` in the GitHub release works.
* Merge to `master` (docs auto-publish).
#### Patch releases, Go toolchain updates, APT/RPM Package rebuilds #### Patch releases, Go toolchain updates, APT/RPM Package rebuilds
+1
View File
@@ -7,6 +7,7 @@
<div class="rst-other-versions"> <div class="rst-other-versions">
<dl> <dl>
<dt>Releases</dt> <dt>Releases</dt>
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.7.0">v0.7.0</a></dd>
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.6.1">v0.6.1</a></dd> <dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.6.1">v0.6.1</a></dd>
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.5.0">v0.5.0</a></dd> <dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.5.0">v0.5.0</a></dd>
<dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.4.0">v0.4.0</a></dd> <dd><a href="https://github.com/zrepl/zrepl/releases/tag/v0.4.0">v0.4.0</a></dd>
+7
View File
@@ -16,6 +16,8 @@ Changelog
0.7.0 0.7.0
----- -----
`GitHub Release <https://github.com/zrepl/zrepl/releases/tag/v0.7.0>`_
* |feature| Config file inclusion using ``include`` directive. * |feature| Config file inclusion using ``include`` directive.
This allows distributing zrepl job definitions across multiple YAML files in a ``conf.d`` style directory. This allows distributing zrepl job definitions across multiple YAML files in a ``conf.d`` style directory.
(:commit:`4d6583e`, thanks, `@ZeyadTamimi <https://github.com/zeyadtamimi>`_). (:commit:`4d6583e`, thanks, `@ZeyadTamimi <https://github.com/zeyadtamimi>`_).
@@ -26,10 +28,15 @@ Changelog
* |bugfix| Detect duplicate and internal job names at config parse time. * |bugfix| Detect duplicate and internal job names at config parse time.
zrepl will now refuse to load a config with duplicate or internally reserved job names, instead of failing later at daemon startup. zrepl will now refuse to load a config with duplicate or internally reserved job names, instead of failing later at daemon startup.
(:commit:`860a9be`). (:commit:`860a9be`).
* |bugfix| Fix ``last_n`` keep rule behavior (:commit:`ebc46cf`, thanks, `@dsh2dsh <https://github.com/dsh2dsh>`_).
* |bugfix| Allow different jobs to use the same ``root_fs`` for sinks (:commit:`27012e5`, thanks, `@dsh2dsh <https://github.com/dsh2dsh>`_).
* |docs| Add installation instructions for openSUSE (:commit:`40c4827`, thanks, `@findesgh <https://github.com/findesgh>`_). * |docs| Add installation instructions for openSUSE (:commit:`40c4827`, thanks, `@findesgh <https://github.com/findesgh>`_).
* |docs| Improve documentation on ``send_properties`` configuration and NFS/SMB considerations (:commit:`b5d8538`, thanks, `@Malvineous <https://github.com/Malvineous>`_). * |docs| Improve documentation on ``send_properties`` configuration and NFS/SMB considerations (:commit:`b5d8538`, thanks, `@Malvineous <https://github.com/Malvineous>`_).
* |docs| Warn more prominently about the risks of the ``not_replicated`` keep rule (:commit:`affe00a`, thanks, `@wxiaoguang <https://github.com/wxiaoguang>`_). * |docs| Warn more prominently about the risks of the ``not_replicated`` keep rule (:commit:`affe00a`, thanks, `@wxiaoguang <https://github.com/wxiaoguang>`_).
* |docs| Improve TLS/EasyRSA setup instructions (:commit:`e524b60`, thanks, `@alorimer <https://github.com/alorimer>`_). * |docs| Improve TLS/EasyRSA setup instructions (:commit:`e524b60`, thanks, `@alorimer <https://github.com/alorimer>`_).
* |docs| Remove dead Bountysource link (:commit:`440b074`, thanks, `@lpulley <https://github.com/lpulley>`_).
* |docs| Fix missing newline in compile-from-source documentation (:commit:`e2fcf9f`, thanks, `@Raupinger <https://github.com/Raupinger>`_).
* |docs| Fix apt repository installation snippet (:commit:`8305367`, thanks, `@fermino <https://github.com/fermino>`_).
* |docs| ``zrepl.github.io``: auto-publish from ``master`` branch, retire ``stable`` branch (:commit:`4f950bb`). * |docs| ``zrepl.github.io``: auto-publish from ``master`` branch, retire ``stable`` branch (:commit:`4f950bb`).
* |maint| ``zrepl status``: switch from the unmaintained ``cview`` fork back to the actively maintained ``tview`` library (:commit:`d7ede3f`). * |maint| ``zrepl status``: switch from the unmaintained ``cview`` fork back to the actively maintained ``tview`` library (:commit:`d7ede3f`).
* |maint| Update to Go 1.25 toolchain with Go 1.24 language level & Go dependencies * |maint| Update to Go 1.25 toolchain with Go 1.24 language level & Go dependencies
+24
View File
@@ -32,6 +32,30 @@ We would like to thank the following people and organizations for supporting zre
<div class="fa fa-code" style="width: 1em;"></div> <div class="fa fa-code" style="width: 1em;"></div>
..
The list below is (roughly) sorted by date of latest contribution, newest first.
..
↓ post v0.7.0
* |supporter-std| `drbawb <https://git.sr.ht/~hime>`_
..
↓ claude --permission-mode default /update-supporters v0.6.1..v0.7.0
* |supporter-code| `Bakhtiyar Neyman <https://github.com/bakhtiyarneyman>`_
* |supporter-code| `Zeyad Tamimi <https://github.com/ZeyadTamimi>`_
* |supporter-code| `Orsiris de Jong <https://github.com/deajan>`_
* |supporter-code| `Martin Glatzle <https://github.com/findesgh>`_
* |supporter-code| `Andrew Lorimer <https://github.com/alorimer>`_
* |supporter-code| `Adam Nielsen <https://github.com/Malvineous>`_
* |supporter-code| `wxiaoguang <https://github.com/wxiaoguang>`_
* |supporter-code| `Logan Pulley <https://github.com/lpulley>`_
* |supporter-code| `Florian <https://github.com/Raupinger>`_
* |supporter-code| `Fermín Olaiz <https://github.com/fermino>`_
* |supporter-code| `Denis Shaposhnikov <https://github.com/dsh2dsh>`_
..
↓ Before /update-supporters
* |supporter-gold| `Hostsharing eG die Hosting-Genossenschaft <https://www.hostsharing.net/>`_ * |supporter-gold| `Hostsharing eG die Hosting-Genossenschaft <https://www.hostsharing.net/>`_
* |supporter-std| `Max Christian Pohle <https://coderonline.de>`_ * |supporter-std| `Max Christian Pohle <https://coderonline.de>`_
* |supporter-gold| Prominic.NET, Inc. * |supporter-gold| Prominic.NET, Inc.
+42 -1
View File
@@ -635,6 +635,25 @@ func (f subroot) UserSpecifiedDatasets() zfs.UserSpecifiedDatasetsSet {
} }
} }
// senderPrefixDatasetPath returns the configured literal sender-side prefix
// (e.g. "hdd_9XG1J9J1") to strip from incoming filesystem paths, or nil if
// ZREPL_STRIP_SENDER_PREFIX is unset/empty. Using a literal string (rather
// than a component count) lets us losslessly reconstruct the sender's
// original path later in ListFilesystems, which is required for the
// sender/receiver filesystem-matching in the replication planner to work.
func senderPrefixDatasetPath() *zfs.DatasetPath {
s := envconst.String("ZREPL_STRIP_SENDER_PREFIX", "")
if s == "" {
return nil
}
dp, err := zfs.NewDatasetPath(s)
if err != nil || dp.Length() == 0 {
return nil
}
return dp
}
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) { func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
p, err := zfs.NewDatasetPath(fs) p, err := zfs.NewDatasetPath(fs)
if err != nil { if err != nil {
@@ -643,11 +662,17 @@ func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
if p.Length() == 0 { if p.Length() == 0 {
return nil, errors.Errorf("cannot map empty filesystem") return nil, errors.Errorf("cannot map empty filesystem")
} }
// PATCH: strip a configured literal prefix (e.g. the source zpool name)
// from the *sender's* path before appending it under root_fs.
if prefix := senderPrefixDatasetPath(); prefix != nil && p.HasPrefix(prefix) {
p.TrimPrefix(prefix)
}
c := f.localRoot.Copy() c := f.localRoot.Copy()
c.Extend(p) c.Extend(p)
return c, nil return c, nil
} }
func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) { func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error) {
defer trace.WithSpanFromStackUpdateCtx(&ctx)() defer trace.WithSpanFromStackUpdateCtx(&ctx)()
@@ -688,12 +713,28 @@ func (s *Receiver) ListFilesystems(ctx context.Context, req *pdu.ListFilesystemR
a.TrimPrefix(root) a.TrimPrefix(root)
// PATCH: re-prepend the literal sender prefix that MapToLocal strips,
// so that the Path we report here matches what the sender reports
// for the same filesystem (see replication_logic.go's Path-based
// sender/receiver matching). Without this, the planner can never
// find a matching receiverFS and will treat every cycle as if the
// filesystem doesn't exist yet on the receiver.
reportPath := a.ToString()
if prefix := senderPrefixDatasetPath(); prefix != nil {
if reportPath == "" {
reportPath = prefix.ToString()
} else {
reportPath = prefix.ToString() + "/" + reportPath
}
}
fs := &pdu.Filesystem{ fs := &pdu.Filesystem{
Path: a.ToString(), Path: reportPath,
IsPlaceholder: ph.IsPlaceholder, IsPlaceholder: ph.IsPlaceholder,
ResumeToken: token, ResumeToken: token,
} }
fss = append(fss, fs) fss = append(fss, fs)
} }
if len(fss) == 0 { if len(fss) == 0 {
getLogger(ctx).Debug("no filesystems found") getLogger(ctx).Debug("no filesystems found")
+177
View File
@@ -0,0 +1,177 @@
https://claude.ai/chat/f688c411-b11e-4455-918b-fde5e6577430
Diagnosed replication snapshot duplication caused by version listing failures
Diagnosed replication snapshot duplication caused by version listing failures
Отличная зацепка — снапшот chunk_9XG1J8T0/nomad/ha-zah@zrepl_20260724_054520_000 реально существует (0B allocated, 195M refer — то есть данные там есть), но планировщик каждый цикл продолжает считать, что на приёмнике версий нет вообще. Значит проблема не в наличии данных, а в том, что ListFilesystemVersions (то, чем сверяется планировщик) их почему-то не видит. Погружусь в код листинга снапшотов на ресивере.
Ran 3 commands, viewed 3 files
Ran 3 commands, viewed 3 files
Кажется, нашёл настоящий баг — и это очень похоже на правду. Проверю до конца, как формируется список receiverFS/IsPlaceholder, которым пользуется планировщик (не тот же код, что читает placeholder при самом receive).
Viewed 5 files, ran 4 commands
Viewed 5 files, ran 4 commands
Нашёл настоящую причину, и она серьёзнее, чем казалось — это баг именно от моего патча, а не от zrepl самого по себе. Дело в сопоставлении списков sender/receiver в планировщике.
Проверяю, есть ли envconst.String для строкового значения
Проверяю, есть ли envconst.String для строкового значения
Вот и реальный баг — и он от патча
Планировщик (replication_logic.go:302-306) сопоставляет sender и receiver filesystem'ы строковым сравнением путей:
go
for _, rfs := range rfss {
if rfs.Path == fs.Path { // fs.Path — путь СЕНДЕРА, напр. "hdd_9XG1J9J1/nomad/ha-zah"
receiverFS = rfs
}
}
А rfs.Path формируется в Receiver.ListFilesystems (endpoint.go:695) как a.TrimPrefix(root) — локальный путь chunk_9XG1J8T0/nomad/ha-zah минус root_fs = nomad/ha-zah.
До патча это совпадало с sender-путём один-в-один (root_fs + sender_path → обрезаем root_fs → получаем обратно sender_path). А теперь, когда MapToLocal откусывает ещё и hdd_9XG1J9J1, обратная обрезка даёts nomad/ha-zah — а сендер сообщает hdd_9XG1J9J1/nomad/ha-zah. Строки не совпадают → receiverFS всегда nil → планировщик каждый раз считает, что на приёмнике версий нет → пытается full receive → ловит "destination exists". Именно поэтому цикл повторяется бесконечно.
Чиню — добавляю обратное восстановление отрезанного префикса именно в отчёт ListFilesystems, чтобы сопоставление путей снова работало:
Architected shared helper function to consolidate prefix-handling logic
Architected shared helper function to consolidate prefix-handling logic
Есть всё нужное. Переписываю патч целиком на литеральный строковый префикс — это устраняет и проблему с расчётом пути, и делает обратное сопоставление в ListFilesystems возможным:
Reconciled edge case behavior and prepared filesystem prefix restoration
Reconciled edge case behavior and prepared filesystem prefix restoration
Смотрю текущее состояние MapToLocal перед переработкой
Переключаю strip с "N компонентов" на литеральный строковый префикс, чтобы его можно было симметрично восстановить в ListFilesystems для сопоставления путей с sender'ом
endpoint.go
+35
-18
Reconciled edge case behavior and prepared filesystem prefix restoration.
Done
Теперь патчу ListFilesystems, чтобы при формировании отчётного пути (используемого для сверки с sender) обрезанный префикс восстанавливался обратно:
Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле
Восстанавливаю отрезанный sender-префикс в пути, который ListFilesystems возвращает планировщику — иначе receiver Path не совпадает с sender Path и планировщик считает receiver пустым на каждом цикле
Проверяю синтаксис и то, что старая переменная ZREPL_STRIP_SENDER_PREFIX_COMPS больше нигде не используется:
Проверяю синтаксис и итоговое использование переменных окружения
Проверяю синтаксис и итоговое использование переменных окружения
Синтаксис чистый, старой переменной нигде не осталось. Собираю (насколько позволяет toolchain в песочнице):
Ran 2 commands
Ran 2 commands
Отлично, всё корректно, включая граничный случай равных путей. Патч готов. Итог:
Что было не так
Планировщик сопоставляет filesystem'ы sender'а и receiver'а строковым равенством путей (replication_logic.go:303: rfs.Path == fs.Path). До патча receiver.Path (локальный путь минус root_fs) всегда совпадал с sender.Path один в один. Мой первый патч ломал этот инвариант: обрезав ещё и hdd_9XG1J9J1 при записи, но не восстанавливая его при отчёте — receiver стал сообщать "nomad/ha-zah", а sender — "hdd_9XG1J9J1/nomad/ha-zah". Строки никогда не совпадали → receiverFS всегда nil → планировщик каждый цикл считал, что на приёмнике ничего нет → пытался полный recv в уже занятый датасет → ошибка. Бесконечный цикл был не багом zrepl, а прямым следствием несимметричности моего же патча.
Что изменилось
Заменил обрезку "N компонентов" на обрезку литерального строкового префикса — это делает операцию обратимой.
В MapToLocal префикс отрезается при вычислении локального пути хранения.
В ListFilesystems тот же префикс приклеивается обратно к пути, который сообщается планировщику — так путь снова совпадает с тем, что говорит sender, и сопоставление работает.
Как применить
Пересоберите бинарник, затем поменяйте имя переменной в systemd-юните (старое ZREPL_STRIP_SENDER_PREFIX_COMPS=1 → новое, с явным значением префикса):
ini
Environment=ZREPL_STRIP_SENDER_PREFIX=hdd_9XG1J9J1
bash
systemctl daemon-reload
systemctl restart zrepl_rcv
Датасеты, скорее всего, снова придётся снести (zfs destroy -r chunk_9XG1J8T0/nomad) и дать реплицироваться с нуля, раз в прошлый раз всё было в неконсистентном состоянии из-за бага. После рестарта проверьте:
bash
journalctl -u zrepl_rcv -n 100 --no-pager | grep -iE "conflict|receive failed|error"
Ключевой признак, что теперь всё работает правильно: строка "conflict automatically resolved" с пустым sorted receiver versions должна появиться только один раз (при первой инициализации датасета), а не повторяться на каждом цикле после успешного приёма.
а где патч?
Показываю итоговый diff патча
Показываю итоговый diff патча
дай исправленные функции
Показываю итоговый код исправленных функций целиком
Показываю итоговый код исправленных функций целиком
Вот три исправленные функции целиком (файл internal/endpoint/endpoint.go):
1. Новая вспомогательная функция (строки 638–654):
go
// senderPrefixDatasetPath returns the configured literal sender-side prefix
// (e.g. "hdd_9XG1J9J1") to strip from incoming filesystem paths, or nil if
// ZREPL_STRIP_SENDER_PREFIX is unset/empty. Using a literal string (rather
// than a component count) lets us losslessly reconstruct the sender's
// original path later in ListFilesystems, which is required for the
// sender/receiver filesystem-matching in the replication planner to work.
func senderPrefixDatasetPath() *zfs.DatasetPath {
s := envconst.String("ZREPL_STRIP_SENDER_PREFIX", "")
if s == "" {
return nil
}
dp, err := zfs.NewDatasetPath(s)
if err != nil || dp.Length() == 0 {
return nil
}
return dp
}
2. MapToLocal (строки 656672):
go
func (f subroot) MapToLocal(fs string) (*zfs.DatasetPath, error) {
p, err := zfs.NewDatasetPath(fs)
if err != nil {
return nil, err
}
if p.Length() == 0 {
return nil, errors.Errorf("cannot map empty filesystem")
}
// PATCH: strip a configured literal prefix (e.g. the source zpool name)
// from the *sender's* path before appending it under root_fs.
if prefix := senderPrefixDatasetPath(); prefix != nil && p.HasPrefix(prefix) {
p.TrimPrefix(prefix)
}
c := f.localRoot.Copy()
c.Extend(p)
return c, nil
}
3. Кусок ListFilesystems, где формируется Path (строки 712–734, остальная часть функции без изменений):
go
a.TrimPrefix(root)
// PATCH: re-prepend the literal sender prefix that MapToLocal strips,
// so that the Path we report here matches what the sender reports
// for the same filesystem (see replication_logic.go's Path-based
// sender/receiver matching). Without this, the planner can never
// find a matching receiverFS and will treat every cycle as if the
// filesystem doesn't exist yet on the receiver.
reportPath := a.ToString()
if prefix := senderPrefixDatasetPath(); prefix != nil {
if reportPath == "" {
reportPath = prefix.ToString()
} else {
reportPath = prefix.ToString() + "/" + reportPath
}
}
fs := &pdu.Filesystem{
Path: reportPath,
IsPlaceholder: ph.IsPlaceholder,
ResumeToken: token,
}
fss = append(fss, fs)
Больше в файле ничего не менялось — MapToLocal вызывается ещё и из Receive/ListFilesystemVersions/DestroySnapshots, но там аргументом всегда приходит исходный путь сендера (req.Filesystem), так что дополнительных правок там не требуется.
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
#
# zrepl_last_snapshot_age.sh
#
# Показывает, сколько минут назад был создан последний снапшот
# (по умолчанию с префиксом "zrepl_") на резервной (приёмной) стороне.
#
# Использование:
# ./zrepl_last_snapshot_age.sh <датасет> [<датасет2> ...]
# ./zrepl_last_snapshot_age.sh -r <датасет> # рекурсивно по всем дочерним
# ./zrepl_last_snapshot_age.sh -p zrepl_ -r chunk_9XG1J8T0/nomad
#
# Опции:
# -r рекурсивно проверять все дочерние датасеты
# -p PREFIX фильтровать снапшоты по префиксу имени (по умолчанию "zrepl_")
# -w MINUTES задать порог в минутах: если снапшот старше — exit code 1
# (удобно для мониторинга / cron)
# -q тихий режим: не печатать построчный вывод, только итог
#
# Коды возврата:
# 0 — всё ок (снапшот найден, и если задан -w, он не старше порога)
# 1 — снапшот найден, но старше порога -w
# 2 — снапшот не найден вообще (для датасета/выборки)
# 3 — ошибка использования / zfs недоступен
set -euo pipefail
PREFIX="zrepl_"
RECURSIVE=0
WARN_MINUTES=""
QUIET=0
usage() {
grep '^#' "$0" | sed -e '1d' -e 's/^# \{0,1\}//'
exit 3
}
while getopts ":rp:w:qh" opt; do
case "$opt" in
r) RECURSIVE=1 ;;
p) PREFIX="$OPTARG" ;;
w) WARN_MINUTES="$OPTARG" ;;
q) QUIET=1 ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
if [[ $# -lt 1 ]]; then
echo "Ошибка: укажите хотя бы один датасет." >&2
usage
fi
if ! command -v zfs >/dev/null 2>&1; then
echo "Ошибка: команда zfs не найдена." >&2
exit 3
fi
now_epoch=$(date +%s)
overall_newest_epoch=0
overall_newest_name=""
any_found=0
for dataset in "$@"; do
zfs_opts=(-H -p -o name,creation -t snapshot -s creation)
if [[ $RECURSIVE -eq 1 ]]; then
zfs_opts+=(-r)
else
zfs_opts+=(-d 1)
fi
# -H без заголовков, -p значения в "машинном" виде (creation -> epoch seconds)
mapfile -t lines < <(zfs list "${zfs_opts[@]}" "$dataset" 2>/dev/null | grep "@${PREFIX}" || true)
if [[ ${#lines[@]} -eq 0 ]]; then
if [[ $QUIET -eq 0 ]]; then
echo "[$dataset] снапшотов с префиксом '${PREFIX}' не найдено"
fi
continue
fi
# последняя строка — самый свежий снапшот (сортировка -s creation)
last_line="${lines[-1]}"
snap_name="${last_line%%$'\t'*}"
snap_epoch="${last_line##*$'\t'}"
age_seconds=$(( now_epoch - snap_epoch ))
age_minutes=$(( age_seconds / 60 ))
any_found=1
if (( snap_epoch > overall_newest_epoch )); then
overall_newest_epoch=$snap_epoch
overall_newest_name=$snap_name
fi
if [[ $QUIET -eq 0 ]]; then
printf "[%s] последний снапшот: %s (%d мин. назад)\n" \
"$dataset" "$snap_name" "$age_minutes"
fi
done
if [[ $any_found -eq 0 ]]; then
echo "Ни одного снапшота с префиксом '${PREFIX}' не найдено ни в одном из указанных датасетов." >&2
exit 2
fi
overall_age_minutes=$(( (now_epoch - overall_newest_epoch) / 60 ))
echo "---"
printf "Самый свежий снапшот в выборке: %s (%d мин. назад)\n" \
"$overall_newest_name" "$overall_age_minutes"
if [[ -n "$WARN_MINUTES" ]]; then
if (( overall_age_minutes > WARN_MINUTES )); then
echo "ВНИМАНИЕ: последний снапшот старше порога ${WARN_MINUTES} мин." >&2
exit 1
fi
fi
exit 0
+1 -1
View File
@@ -69,7 +69,7 @@ cp -a internal/config/samples %{buildroot}%{_datadir}/
%{_bindir}/zrepl %{_bindir}/zrepl
%config %{_unitdir}/zrepl.service %config %{_unitdir}/zrepl.service
%dir %{_sysconfdir}/zrepl %dir %{_sysconfdir}/zrepl
%config %{_sysconfdir}/zrepl/zrepl.yml %config(noreplace) %{_sysconfdir}/zrepl/zrepl.yml
%{_datadir}/zsh/site-functions/_zrepl %{_datadir}/zsh/site-functions/_zrepl
%{_datadir}/bash-completion/completions/zrepl %{_datadir}/bash-completion/completions/zrepl
%{_datadir}/doc/zrepl %{_datadir}/doc/zrepl