Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d5d8d04e0 | |||
| 320b394ff8 |
@@ -7,21 +7,13 @@ 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:
|
||||||
cli_yml = Path.home() / ".circleci" / "cli.yml"
|
raise ValueError('CIRCLE_TOKEN environment variable not set')
|
||||||
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')
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
|
|
||||||
[](https://patreon.com/zrepl) [](https://github.com/sponsors/problame) [](https://liberapay.com/zrepl/donate) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
|
||||||
```
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
---
|
|
||||||
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,17 +4,8 @@
|
|||||||
|
|
||||||
ARTIFACTDIR := artifacts
|
ARTIFACTDIR := artifacts
|
||||||
|
|
||||||
ifdef ZREPL_VERSION
|
_ZREPL_VERSION := v0.7.0
|
||||||
_ZREPL_VERSION := $(ZREPL_VERSION)
|
ZREPL_PACKAGE_RELEASE := 2
|
||||||
endif
|
|
||||||
ifndef _ZREPL_VERSION
|
|
||||||
_ZREPL_VERSION := $(shell git describe --always --dirty 2>/dev/null || echo "ZREPL_BUILD_INVALID_VERSION" )
|
|
||||||
ifeq ($(_ZREPL_VERSION),ZREPL_BUILD_INVALID_VERSION) # can't use .SHELLSTATUS because Debian Stretch is still on gmake 4.1
|
|
||||||
$(error cannot infer variable ZREPL_VERSION using git and variable is not overriden by make invocation)
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
ZREPL_PACKAGE_RELEASE := 1
|
|
||||||
|
|
||||||
GO := go
|
GO := go
|
||||||
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
GOOS ?= $(shell bash -c 'source <($(GO) env) && echo "$$GOOS"')
|
||||||
@@ -56,7 +47,7 @@ release-docker-mkcachemount:
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
##################### PRODUCING A RELEASE #############
|
##################### PRODUCING A RELEASE #############
|
||||||
.PHONY: release wrapup-and-checksum check-git-clean verify-and-sign clean ensure-release-toolchain
|
.PHONY: release wrapup-and-checksum check-git-clean 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,8 +202,7 @@ 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)"
|
||||||
|
|
||||||
verify-and-sign:
|
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
|
||||||
@@ -223,7 +213,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/' "$(JOB_NUM)" "$(ARTIFACTDIR)/release"
|
python3 .circleci/download_artifacts.py --prefix 'artifacts/release/' "$(BUILD_NUM)" "$(ARTIFACTDIR)/release"
|
||||||
|
|
||||||
##################### MULTI-ARCH HELPERS #####################
|
##################### MULTI-ARCH HELPERS #####################
|
||||||
|
|
||||||
|
|||||||
@@ -93,50 +93,25 @@ 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 against the `master` branch (trigger via CircleCI UI).
|
* Run the `release` pipeline (trigger via CircleCI UI).
|
||||||
This URL: https://app.circleci.com/pipelines/github/zrepl/zrepl?branch=master.
|
* Download artifacts: `make download-circleci-release BUILD_NUM=<circleci-build-number>`
|
||||||
Example pipeline: https://app.circleci.com/pipelines/github/zrepl/zrepl/8547
|
* Create GitHub release and upload artifacts:
|
||||||
* Download artifacts using this handy makefile target.
|
|
||||||
Note: `JOB_NUM` must be the **job number** from the `release-upload` job, **not the pipeline number**.
|
|
||||||
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
|
|
||||||
|
|
||||||
```
|
|
||||||
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
|
```bash
|
||||||
claude --permission-mode default '/draft-release v0.7.0'
|
gh release create vX.Y.Z --title "vX.Y.Z" --notes "See changelog" --draft
|
||||||
|
gh release upload vX.Y.Z artifacts/release/*
|
||||||
```
|
```
|
||||||
This command will verify that artifacts are ready, create the draft release, and upload all artifacts.
|
* Review the draft release, edit the changelog, then publish.
|
||||||
* 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
|
||||||
|
|
||||||
|
|||||||
Vendored
-1
@@ -7,7 +7,6 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ 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>`_).
|
||||||
@@ -28,15 +26,10 @@ 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
|
||||||
|
|||||||
@@ -32,30 +32,6 @@ 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.
|
||||||
|
|||||||
@@ -635,25 +635,6 @@ 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 {
|
||||||
@@ -662,17 +643,11 @@ 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)()
|
||||||
|
|
||||||
@@ -713,28 +688,12 @@ 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: reportPath,
|
Path: a.ToString(),
|
||||||
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")
|
||||||
@@ -1144,4 +1103,4 @@ func doDestroySnapshots(ctx context.Context, lp *zfs.DatasetPath, snaps []*pdu.F
|
|||||||
return &pdu.DestroySnapshotsRes{
|
return &pdu.DestroySnapshotsRes{
|
||||||
Results: ress,
|
Results: ress,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
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 (строки 656–672):
|
|
||||||
|
|
||||||
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), так что дополнительных правок там не требуется.
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Reference in New Issue
Block a user