Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: cleanup-completed-issues
description: Guide for archiving closed issue specification files from docs/issues/open/ to docs/issues/closed/. Covers verifying closure on GitHub, moving files, updating frontmatter, auditing and repairing affected documentation links, creating a branch, and opening a PR. Permanent deletion of closed specs is not automated — the user must explicitly request it. Use when cleaning up closed issue specs, archiving issue docs, or maintaining the docs/issues/ folder. Triggers on "cleanup issue", "archive issue", "move closed issue", "clean completed issues", or "maintain issue docs".
metadata:
author: torrust
version: "1.10"
version: "1.11"
---

# Cleaning Up Completed Issues
Expand All @@ -25,6 +25,8 @@ Related lifecycle docs:

- Open issue specs: [`docs/issues/open/README.md`](../../../../../docs/issues/open/README.md)
- Closed issue buffer: [`docs/issues/closed/README.md`](../../../../../docs/issues/closed/README.md)
- Reopening an archived issue or EPIC: [reopen-issue](../reopen-issue/SKILL.md)
Comment thread
josecelano marked this conversation as resolved.
<!-- skill-link: reopen-issue -->

## When to Archive

Expand Down
149 changes: 149 additions & 0 deletions .github/skills/dev/planning/reopen-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
name: reopen-issue
description: Guide for reopening a closed GitHub issue or EPIC whose specification was archived in docs/issues/closed/. Covers deciding whether to reopen or file new work, reopening on GitHub, linking a new subissue, moving the spec back to docs/issues/open/, migrating legacy frontmatter, resetting closure checkpoints, and repairing live references. Use when new work belongs to a closed EPIC or a closed issue regressed or was incomplete. Triggers on "reopen issue", "reopen EPIC", "add subissue to closed EPIC", "unarchive issue spec", or "move spec back to open".
metadata:
author: torrust
version: "1.1"
---

<!-- skill-link: reopen-issue -->

# Reopening a Closed Issue or EPIC

This is the inverse of [cleanup-completed-issues](../cleanup-completed-issues/SKILL.md). It was
first used to reopen configuration EPIC #1978 for subissue #2466 (2026-10-07). The EPIC path is
exercised; the single-issue path is derived from it and should be refined on first use.
Comment thread
josecelano marked this conversation as resolved.

## Step 1: Decide Whether to Reopen

Reopen only with explicit maintainer approval; reopening changes shared GitHub state.

Reopen when the new work belongs to the original goal and its deliverable is not yet released (for
example, #1978's configuration schema v3.0.0 was active but not published on crates.io). Otherwise
create a new issue or EPIC and reference the closed one.

For a closed single issue, reopen when the closing PR did not meet its acceptance criteria or the
fix regressed before release. A regression after release is a new bug issue.

## Step 2: Prepare the Branch

Update `develop` first, check that it is not behind, and only then create the branch. Run the
behind count as its own step and read it: a skipped or failed `git pull --ff-only` leaves `develop`
behind, and only the count shows it. Chained with `&&`, `git checkout -b` runs even when the count
is non-zero.

```bash
UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-torrust}"
git checkout develop
git pull --ff-only "$UPSTREAM_REMOTE" develop
git rev-list --count HEAD.."$UPSTREAM_REMOTE"/develop # must print 0
git checkout -b <branch>
```

When reopening an EPIC to add a subissue, reuse that subissue's specification branch
(`<issue>-<epic>-<description>-spec`), so the reopen, the subissue spec, and any ADR land in one PR.

## Step 3: Update GitHub

1. When adding a subissue, create it first (see [create-issue](../create-issue/SKILL.md)), so the
reopen comment can cite it.
2. Reopen with the reason:

```bash
gh issue reopen <number> --repo torrust/torrust-tracker --comment "Reopened to <reason>."
```

3. Link a new subissue natively; the REST endpoint needs the child's database `id`, not its number:

```bash
CHILD_ID=$(gh api repos/torrust/torrust-tracker/issues/<child> --jq .id)
gh api -X POST repos/torrust/torrust-tracker/issues/<epic>/sub_issues -F sub_issue_id="$CHILD_ID"
```

4. Check the reopened issue's body for a `Specification` link and point it at the `open/` path.
Repository searches do not see issue bodies, and older bodies may still name a legacy
single-file path:

```bash
gh issue view <number> --repo torrust/torrust-tracker --json body --jq .body > .tmp/issue-<number>-body.md
# edit the link, then:
gh issue edit <number> --repo torrust/torrust-tracker --body-file .tmp/issue-<number>-body.md
```

## Step 4: Move the Specification Back

```bash
git mv docs/issues/closed/<folder> docs/issues/open/
```

Then update the moved spec:

- **Frontmatter**: set `status` to `planned` or `in-progress`, `spec-path` to the `open/` path, and
`last-updated-utc` to the current time. Specs archived before the v1 schema must be migrated to
v1 in `open/` (`schema-version: 1`, `epic: null` where applicable, quoted `last-updated-utc`, no
trailing slash in `related-artifacts`). Follow the migration checklist in
[`contrib/dev-tools/checks/frontmatter-validator/README.md`](../../../../../contrib/dev-tools/checks/frontmatter-validator/README.md).
- **Archive-time edits**: revert body paths the archive rewrote to `closed/`, such as the
"spec drafted in" checkpoint.
- **Checkpoints**: clear the "issue closed and spec moved" and "acceptance criteria reviewed" boxes.
Comment thread
josecelano marked this conversation as resolved.
- **Acceptance Verification**: set rows that depended on all work being done back to `TODO`, clear
the matching Acceptance Criteria checkboxes (for example, "Epic status reflects actual state of
linked subissues"), and correct stale counts (for example, the number of linked subissues).
- **Subissues table** (EPIC): add the new row with `TODO`. A `related-artifacts` entry must name a
tracked file, so add the new child spec path in the commit that adds the spec.
- **Progress Log**: add an entry with the reason and the remaining work. Date each entry at the
action it records: the maintainer's decision, the GitHub reopen, and the move are separate
events.
- **References**: add the new subissue, the issue it came from, and any new ADR.
- **Parent EPIC** (single issue): set its row back to `IN_PROGRESS` and point it at `open/`.

The frontmatter validator checks **staged** content. Stage the edits, not only the rename, before
running it:

```bash
git add -A docs/issues/open/<folder>
cargo run --quiet --package frontmatter-validator --bin frontmatter-validator -- --staged
```

## Step 5: Repair Live References

```bash
rg '<folder>' --glob '!target/**' --glob '!storage/**'
```

- Update live navigational references outside `docs/issues/closed/` (for example, package docs) to
the `open/` path.
Comment thread
josecelano marked this conversation as resolved.
- Leave links inside other closed specs and historical PR review records unchanged. They are
archived records, the [local link checker](../../../../../lychee.toml) excludes
`docs/issues/closed/`, and the spec returns
there on closure.

When the reopened issue closes again, archive it with
[cleanup-completed-issues](../cleanup-completed-issues/SKILL.md); its reference search flips the
live references back.

## Step 6: Commit and Open the PR

Commit the reopen separately from the new subissue spec. When the reopened spec references a new
ADR or spec in `related-artifacts`, commit that artifact first, because the validator requires
tracked paths.

```bash
git commit -S -m "docs(issues): [#<number>] reopen <short title> to <reason>"
```

Use `Related to #<number>` in the PR body, never a closing keyword.

## Skill Links

Artifacts this skill depends on; the first three carry a `skill-link: reopen-issue` marker:

- [`cleanup-completed-issues`](../cleanup-completed-issues/SKILL.md): the inverse workflow; a
change to how specs are archived changes what a reopen must undo.
- [`frontmatter-validator/README.md`](../../../../../contrib/dev-tools/checks/frontmatter-validator/README.md):
the v1 migration checklist used in Step 4.
- [`lychee.toml`](../../../../../lychee.toml): the `docs/issues/closed/` exclusion that Step 5
relies on.
- [`docs/templates/EPIC.md`](../../../../../docs/templates/EPIC.md) and
[`docs/templates/ISSUE.md`](../../../../../docs/templates/ISSUE.md): the checkpoint wording reset
in Step 4. They carry no marker, because specs copy template markers.
2 changes: 2 additions & 0 deletions contrib/dev-tools/checks/frontmatter-validator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ A staged edit to a draft or open `ISSUE.md` or `EPIC.md` without v1 frontmatter

Re-run the validator on the file until it exits `0`.

<!-- skill-link: reopen-issue -->

## Temporary Placement

This crate is approved early work under EPIC #2003, which owns the long-term automation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
semantic-links:
skill-links:
- create-adr
related-artifacts:
- "issue #1978"
- "issue #2245"
- packages/primitives/src/announce.rs
Comment thread
josecelano marked this conversation as resolved.
- docs/adrs/20260723184019_separate_configuration_value_invariants_from_consistency_validation.md
- docs/adrs/20260721100000_use_newtypes_for_constrained_configuration_field_types.md
---

<!-- skill-link: create-adr -->

# Bound Protocol-Agnostic Values by the Tightest Delivery Protocol

## Scope

Repository-level. The decision spans the configuration schema, the domain types in `primitives`,
and every delivery-protocol package (`udp-server`, `http-protocol`, `axum-http-server`), so it
belongs in `docs/adrs/`.

## Description

The tracker serves the same swarm through several delivery protocols: UDP (BEP 15), HTTP (BEP 3
and BEP 23), and possibly others later, such as WebTorrent. Some configured values are sent to
clients by every protocol, but each protocol encodes them differently. The announce interval is the
first case: BEP 15 encodes it as a signed 32-bit integer, while HTTP encodes it as a bencode
integer (`i64` in `http-protocol`).

The configuration and domain types held the interval as a plain `u32`, so a value above
`i32::MAX` was accepted. Before #2245, UDP sent such a value as a negative number. #2245 clamped
it on the UDP wire, which hid the problem: UDP and HTTP clients then received different intervals
for the same configuration.

The [value-invariant ADR](20260723184019_separate_configuration_value_invariants_from_consistency_validation.md)
says *how* to reject a single-value bound: a typed newtype, rejected during deserialization. It
does not say *which* bound to choose when the delivery protocols disagree, or where such a type
lives.

## Agreement

A configuration value that every delivery protocol sends to clients is **one protocol-agnostic
value**, not one value per protocol.

Its bound is the **tightest limit among the supported delivery protocols**. For the announce
interval that is `i32::MAX` seconds (about 68 years), from BEP 15; realistic values are minutes to
hours.

The bound is a domain rule (the value must be representable on every supported protocol), so it is
enforced by a validated newtype in `primitives`. The configuration uses that domain type directly,
as it already uses `AnnouncePolicy`. Following the value-invariant ADR, construction and
`Deserialize` reject out-of-range values, so an invalid configuration fails at load.

Delivery adapters then convert the value to their wire type without clamping or failing. Clamping
remains appropriate only for values that no configuration bounds, such as peer counts.

When a new delivery protocol has a tighter limit, the shared bound is lowered. That is a breaking
configuration change and must be released as one.

This rule covers values the operator configures once and the tracker sends through every protocol.
It does not cover limits on what clients send, which differ per protocol for their own reasons; see
[Cap scrape info hashes per protocol](20261005124222_cap_scrape_info_hashes_per_protocol.md).

## Alternatives Considered

### One configuration value per delivery protocol

Rejected. It duplicates a single concept across protocols and lets operators configure different
intervals for the same swarm by accident.

### Validate the shared value per delivery protocol

Rejected. A value could be valid for HTTP and invalid for UDP, so enabling a protocol would turn a
working configuration into an invalid one. It also needs consistency rules that depend on which
protocols are enabled.

### Clamp on the wire only

Rejected. This was the state after #2245. It silently changes the configured value for some
clients, and the protocols then disagree.

### An arbitrary "sensible" cap

Rejected. A cap such as one day has no source to derive it from, and choosing it is a separate
policy decision. The protocol limit is objective.

### A bounded schema type only in `configuration`

Rejected. It keeps `primitives` unaware of the bound, but the domain type stays unbounded, so the
delivery adapters still need a fallible conversion or a clamp.

## Consequences

- **Positive**: one unambiguous value per concept; all delivery protocols report the same value.
- **Positive**: invalid configurations fail at load with a clear error instead of being clamped.
- **Positive**: delivery adapters convert infallibly, with no clamp to keep in sync.
- **Negative**: the bound is lower than some protocols need. For the announce interval this costs
nothing in practice.
- **Negative**: adding a protocol with a tighter limit is a breaking configuration change.
- **Negative**: changing a `primitives` field type is a breaking public API change for its
consumers.

## Affected Code

- [`packages/primitives/src/announce.rs`](../../packages/primitives/src/announce.rs):
`AnnouncePolicy::interval` and `interval_min`, which #2466 changes to the bounded type.
- [`packages/udp-server/src/handlers/announce.rs`](../../packages/udp-server/src/handlers/announce.rs):
the UDP response encodes the interval through `saturating_wire_i32` until #2466 converts it
without clamping.

Issue #2466 adds module-level doc comments in both places that link back to this ADR.

## Date

2026-10-07

## References

- [Configuration Overhaul EPIC #1978](https://github.com/torrust/torrust-tracker/issues/1978)
- [Issue #2245](https://github.com/torrust/torrust-tracker/issues/2245) — review numeric protocol wire conversions (introduced the UDP clamp)
- [BEP 15: UDP Tracker Protocol](https://www.bittorrent.org/beps/bep_0015.html)
- [Separate Configuration Value Invariants from Consistency Validation](20260723184019_separate_configuration_value_invariants_from_consistency_validation.md)
- [Use Newtypes for Domain-Constrained Configuration Field Types](20260721100000_use_newtypes_for_constrained_configuration_field_types.md)
1 change: 1 addition & 0 deletions docs/adrs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ supersession rules.
| [20261002173716](20261002173716_load_persisted_scrape_downloads_with_a_batched_uncached_lookup.md) | 2026-10-02 | Load persisted scrape downloads with a batched, uncached lookup | Announce and scrape share one persisted-downloads lookup; scrape loads all torrents absent from memory with one deduplicated `IN (...)` query, bounded at 100 info-hashes independently of protocol request limits, with no cache until metrics show a need. |
| [20261005124222](20261005124222_cap_scrape_info_hashes_per_protocol.md) | 2026-10-05 | Cap scrape info hashes per protocol, each for its own reason | UDP and HTTP parsers each keep the first N scrape info hashes and ignore the rest: UDP 74, computed from the packet size; HTTP 100, an abuse-mitigation policy value. Core has no cap, and a per-request cap is not a substitute for rate limiting. |
| [20261005145329](20261005145329_return_result_only_for_concretely_fallible_public_apis.md) | 2026-10-05 | Return `Result` only for concretely fallible public APIs | A public API returns `Result` only when it can fail today, crosses an I/O boundary, or is a trait or port with an existing or planned backend that can fail; otherwise it returns a plain value and a real failure later is a semver-signalled breaking change. No `Infallible` or empty error enums as placeholders; existing public error enums get `#[non_exhaustive]` before first publish. |
| [20261007082938](20261007082938_bound_protocol_agnostic_values_by_the_tightest_delivery_protocol.md) | 2026-10-07 | Bound protocol-agnostic values by the tightest delivery protocol | A configured value that every delivery protocol sends (such as the announce interval) is one value, bounded by the tightest supported protocol (`i32::MAX` from BEP 15) through a validated `primitives` newtype rejected at configuration load; adapters convert without clamping. Per-protocol values, per-protocol validation, and wire-only clamping are rejected. |

## ADR Lifecycle

Expand Down
1 change: 1 addition & 0 deletions docs/issues/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ Use these skills as the authoritative process definitions:

- Create and maintain issue specs: [`.github/skills/dev/planning/create-issue/SKILL.md`](../../.github/skills/dev/planning/create-issue/SKILL.md)
- Close and archive completed specs: [`.github/skills/dev/planning/cleanup-completed-issues/SKILL.md`](../../.github/skills/dev/planning/cleanup-completed-issues/SKILL.md)
- Reopen an archived issue or EPIC: [`.github/skills/dev/planning/reopen-issue/SKILL.md`](../../.github/skills/dev/planning/reopen-issue/SKILL.md)
1 change: 1 addition & 0 deletions docs/issues/closed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ The authoritative procedure is the cleanup workflow skill below.

- Issues index: [../README.md](../README.md)
- Cleanup workflow source of truth: [`.github/skills/dev/planning/cleanup-completed-issues/SKILL.md`](../../../.github/skills/dev/planning/cleanup-completed-issues/SKILL.md)
- Reopen workflow (moves a spec back to `open/`): [`.github/skills/dev/planning/reopen-issue/SKILL.md`](../../../.github/skills/dev/planning/reopen-issue/SKILL.md)
Loading
Loading