mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2026-04-28 15:07:47 +00:00
Compare commits
4 Commits
java-crede
...
add-feedba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caf45e5250 | ||
|
|
8302eaf162 | ||
|
|
bc8623ea97 | ||
|
|
a082ea0842 |
@@ -1,19 +0,0 @@
|
||||
# XRPL Dev Portal — Claude Code Instructions
|
||||
|
||||
## Quick Reference
|
||||
|
||||
- **Framework:** Redocly Realm
|
||||
- **Production branch:** `master`
|
||||
- **Local preview:** `npm start`
|
||||
|
||||
## Localization
|
||||
|
||||
- Default: `en-US`
|
||||
- Japanese: `ja`
|
||||
- Translations mirror `docs/` structure under `@l10n/<language-code>/`
|
||||
|
||||
## Navigation
|
||||
|
||||
- Update `sidebars.yaml` when adding new doc pages
|
||||
- Blog posts have a separate `blog/sidebars.yaml`
|
||||
- Redirects go in `redirects.yaml`
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"deny": [
|
||||
"Bash(git push *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
name: generate-release-notes
|
||||
description: Generate and sort rippled release notes from GitHub commit history
|
||||
argument-hint: --from <ref> --to <ref> [--date YYYY-MM-DD] [--output <path>]
|
||||
allowed-tools: Bash, Read, Edit, Write, Grep, Glob
|
||||
effort: max
|
||||
---
|
||||
|
||||
# Generate rippled Release Notes
|
||||
|
||||
This skill generates a draft release notes blog post for a new rippled version, then sorts the entries into the correct subsections.
|
||||
|
||||
## Execution constraints
|
||||
|
||||
- **Do NOT write scripts** to sort or process the file. Prefer the Edit tool for targeted changes. Use Write only when replacing large sections that are impractical to edit incrementally.
|
||||
- **Output progress**: Before each major step (generating raw release notes, reviewing file, processing amendments, sorting entries, reformatting, cleanup), output a brief status message so the user can see progress.
|
||||
|
||||
## Step 1: Generate the raw release notes
|
||||
|
||||
Run the Python script from the repo root. Pass through all arguments from `$ARGUMENTS`:
|
||||
|
||||
```bash
|
||||
python3 tools/generate-release-notes.py $ARGUMENTS
|
||||
```
|
||||
|
||||
If the user didn't provide `--from` or `--to`, ask them for the base and target refs (tags or branches).
|
||||
|
||||
The script will:
|
||||
- Fetch the version string from `BuildInfo.cpp`
|
||||
- Fetch all commits between the two refs
|
||||
- Fetch PR details (title, link, labels, files, description) via GraphQL
|
||||
- Compare `features.macro` between refs to identify amendment changes
|
||||
- Auto-sort amendment entries into the Amendments section
|
||||
- Output all other entries as unsorted with full context
|
||||
|
||||
## Step 2: Review the generated file
|
||||
|
||||
Read the output file (path shown in script output). Note the **Full Changelog** structure:
|
||||
- **Amendments section**: Contains auto-sorted entries and an HTML comment listing which amendments to include or remove
|
||||
- **Empty subsections**: Features, Breaking Changes, Bug Fixes, Refactors, Documentation, Testing, CI/Build
|
||||
- **Unsorted entries**: After the **Bug Bounties and Responsible Disclosures** section is an unsorted list of entries with title, link, labels, files, and description for context
|
||||
|
||||
## Step 3: Process amendments
|
||||
|
||||
Handle Amendments first, before sorting other entries.
|
||||
|
||||
**3a. Process the auto-sorted Amendments subsection:**
|
||||
The HTML comment contains three lists — follow them exactly:
|
||||
- **Include**: Keep these entries.
|
||||
- **Exclude**: Remove these entries.
|
||||
- Entries on **neither** list: Remove these entries.
|
||||
|
||||
**3b. Scan unsorted entries for unreleased amendment work:**
|
||||
Search through ALL unsorted entries for titles, labels, descriptions, or files that reference amendments on the "Exclude" or "Other amendments not part of this release" lists. Remove entries that directly implement, enable, fix, or refactor these amendments. Keep entries that are general changes that merely reference the amendment as motivation — if the code change is useful on its own regardless of whether the amendment ships, keep it.
|
||||
|
||||
**3c. If you disagree with any amendment decisions, make a note to the user but do NOT deviate from the rules.**
|
||||
|
||||
## Step 4: Sort remaining unsorted entries into subsections
|
||||
|
||||
Move each remaining unsorted entry into the appropriate subsection.
|
||||
|
||||
Use these signals to categorize:
|
||||
|
||||
**Files changed** (strongest signal):
|
||||
- Only `.github/`, `CMakeLists.txt`, `conan*`, CI config files → **CI/Build**
|
||||
- Only `src/test/`, `*_test.cpp` files → **Testing**
|
||||
- Only `*.md`, `docs/` files → **Documentation**
|
||||
|
||||
**Labels** (strong signal):
|
||||
- `Bug` label → **Bug Fixes**
|
||||
|
||||
**Title prefixes** (medium signal):
|
||||
- `fix:` → **Bug Fixes**
|
||||
- `feat:` → **Features**
|
||||
- `refactor:` → **Refactors**
|
||||
- `docs:` → **Documentation**
|
||||
- `test:` → **Testing**
|
||||
- `ci:`, `build:`, `chore:` → **CI/Build**
|
||||
|
||||
**Description content** (when other signals are ambiguous):
|
||||
- Read the PR description to understand the change's purpose
|
||||
- PRs that change API behavior, remove features, or have "Breaking change" checked in their description → **Breaking Changes**
|
||||
|
||||
Additional sorting guidance:
|
||||
- Watch for revert pairs: If a PR was committed and then reverted (or vice versa), check that the net effect is accounted for — don't include both.
|
||||
|
||||
## Step 5: Reformat sorted entries
|
||||
|
||||
After sorting, reformat each entry to match the release notes style.
|
||||
|
||||
**Amendment entries** should follow this format:
|
||||
```markdown
|
||||
- **amendmentName**: Description of what the amendment does. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Use more detail for amendment descriptions since they are the most important. Use present tense.
|
||||
- If there are multiple entries for the same amendment, merge into one, prioritizing the entry that describes the actual amendment.
|
||||
|
||||
**Feature and Breaking Change entries** should follow this format:
|
||||
```markdown
|
||||
- Description of the change. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Keep the description concise. Use past tense.
|
||||
|
||||
**All other entries** should follow this format:
|
||||
```markdown
|
||||
- The PR title of the entry. ([#1234](https://github.com/XRPLF/rippled/pull/1234))
|
||||
```
|
||||
- Copy the PR title as-is. Only fix capitalization, remove conventional commit prefixes (fix:, feat:, ci:, refactor:, docs:, test:, chore:, build:), and adjust to past tense if needed. Do NOT rewrite, paraphrase, or summarize.
|
||||
|
||||
## Step 6: Clean up
|
||||
|
||||
- Add a short and generic description of changes to the existing `seo.description` frontmatter, e.g., "This version introduces new amendments and bug fixes." Do not create long lists of detailed changes.
|
||||
- Add a more detailed summary of the release to the existing "Introducing XRP Ledger Version X.Y.Z" section. Include amendment names (organized in a list if more than 2), featuress, and breaking changes. Limit this to 1 paragraph.
|
||||
- Do NOT delete the **Credits** or **Bug Bounties and Responsible Disclosures** sections
|
||||
- Remove empty subsections that have no entries
|
||||
- Remove all HTML comments (sorting instructions)
|
||||
- Do a final review of the release notes. If you see anything strange, or were forced to take unintuitive actions by these instructions, notify the user, but don't make changes.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -9,7 +9,6 @@ yarn-error.log
|
||||
.venv/
|
||||
_code-samples/*/js/package-lock.json
|
||||
_code-samples/*/go/go.sum
|
||||
_code-samples/*/java/target/
|
||||
_code-samples/*/*/*[Ss]etup.json
|
||||
|
||||
# PHP
|
||||
|
||||
@@ -273,6 +273,8 @@ ul.nav.navbar-nav {
|
||||
--language-picker-background-color: var(--color-gray-8);
|
||||
--select-list-bg-color: var(--color-gray-8);
|
||||
|
||||
--button-bg-color-secondary: var(--color-gray-8);
|
||||
|
||||
--footer-title-text-color: black;
|
||||
--bg-color: var(--color-gray-9);
|
||||
--bg-color-raised: var(--color-gray-8);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
xrpl-py>=3.0.0
|
||||
wxPython==4.2.1
|
||||
toml==0.10.2
|
||||
requests==2.33.0
|
||||
requests==2.32.4
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
# Credential Example (Java)
|
||||
|
||||
This directory contains a Java example demonstrating how to issue a credential, accept a credential, and delete a credential.
|
||||
|
||||
## Setup
|
||||
|
||||
Install dependencies before running any examples:
|
||||
|
||||
```sh
|
||||
mvn install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manage Credentials
|
||||
|
||||
```sh
|
||||
mvn exec:java -Dexec.mainClass=com.example.xrpl.ManageCredentials
|
||||
```
|
||||
|
||||
The script should output two newly funded accounts, the CredentialCreate transaction, CredentialAccept transaction, and CredentialDelete transaction. Each successful transaction submission includes a link to the transaction metadata on the XRPL Explorer.
|
||||
|
||||
```sh
|
||||
=== Funding issuer and subject accounts on Testnet ===
|
||||
|
||||
Issuer: r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL
|
||||
Subject: rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y
|
||||
|
||||
=== Preparing CredentialCreate transaction ===
|
||||
|
||||
{
|
||||
"Account" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
|
||||
"TransactionType" : "CredentialCreate",
|
||||
"Fee" : "15",
|
||||
"Sequence" : 16795444,
|
||||
"LastLedgerSequence" : 16795464,
|
||||
"SigningPubKey" : "EDC2C03C393852514C40CCCCF34CB61A8DDB4AECC6C95271468DDF13DE0979DCC7",
|
||||
"Subject" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
|
||||
"CredentialType" : "6B79632D747261646572"
|
||||
}
|
||||
|
||||
=== Submitting CredentialCreate transaction ===
|
||||
|
||||
CredentialCreate succeeded!
|
||||
Explorer: https://testnet.xrpl.org/transactions/D7A00CFC8DFFE384F7A5D2DF14B3AC5629E8F8DBFD8BD06BC389363782F296B3
|
||||
|
||||
=== Preparing CredentialAccept transaction ===
|
||||
|
||||
{
|
||||
"Account" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
|
||||
"TransactionType" : "CredentialAccept",
|
||||
"Fee" : "15",
|
||||
"Sequence" : 16795444,
|
||||
"LastLedgerSequence" : 16795466,
|
||||
"SigningPubKey" : "EDBED812587E0D7D9F965EFE63F4F2B2BB2EB559AD7D1FA9250C239C235CE62726",
|
||||
"Issuer" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
|
||||
"CredentialType" : "6B79632D747261646572"
|
||||
}
|
||||
|
||||
=== Submitting CredentialAccept transaction ===
|
||||
|
||||
CredentialAccept succeeded!
|
||||
Explorer: https://testnet.xrpl.org/transactions/C9E55B0A5270FEB37C18E710BDB01C46480530673FE8E4FC39FE3D6B036DF8F5
|
||||
|
||||
=== Preparing CredentialDelete transaction ===
|
||||
|
||||
{
|
||||
"Account" : "rs8vtTf3aLZW7XRCh398SUkuuAWBn7q49y",
|
||||
"TransactionType" : "CredentialDelete",
|
||||
"Fee" : "15",
|
||||
"Sequence" : 16795445,
|
||||
"LastLedgerSequence" : 16795468,
|
||||
"SigningPubKey" : "EDBED812587E0D7D9F965EFE63F4F2B2BB2EB559AD7D1FA9250C239C235CE62726",
|
||||
"Issuer" : "r446kRqJA1XGo1zGMiC2RAebtWbQa4duHL",
|
||||
"CredentialType" : "6B79632D747261646572"
|
||||
}
|
||||
|
||||
=== Submitting CredentialDelete transaction ===
|
||||
|
||||
CredentialDelete succeeded!
|
||||
Explorer: https://testnet.xrpl.org/transactions/0755B4FED0A646D5FB3698891D25DC0374C521DEF00D85D8FCFF58EB09CAB4FE
|
||||
```
|
||||
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>credential-samples</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>11</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.xrpl</groupId>
|
||||
<artifactId>xrpl4j-client</artifactId>
|
||||
<version>6.0.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,292 +0,0 @@
|
||||
package com.example.xrpl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.google.common.primitives.UnsignedInteger;
|
||||
import okhttp3.HttpUrl;
|
||||
import org.xrpl.xrpl4j.client.JsonRpcClientErrorException;
|
||||
import org.xrpl.xrpl4j.client.XrplClient;
|
||||
import org.xrpl.xrpl4j.client.faucet.FaucetClient;
|
||||
import org.xrpl.xrpl4j.client.faucet.FundAccountRequest;
|
||||
import org.xrpl.xrpl4j.crypto.keys.KeyPair;
|
||||
import org.xrpl.xrpl4j.crypto.keys.PrivateKey;
|
||||
import org.xrpl.xrpl4j.crypto.keys.Seed;
|
||||
import org.xrpl.xrpl4j.crypto.signing.SignatureService;
|
||||
import org.xrpl.xrpl4j.crypto.signing.SingleSignedTransaction;
|
||||
import org.xrpl.xrpl4j.crypto.signing.bc.BcSignatureService;
|
||||
import org.xrpl.xrpl4j.model.client.accounts.AccountInfoRequestParams;
|
||||
import org.xrpl.xrpl4j.model.client.accounts.AccountInfoResult;
|
||||
import org.xrpl.xrpl4j.model.client.common.LedgerSpecifier;
|
||||
import org.xrpl.xrpl4j.model.client.fees.FeeUtils;
|
||||
import org.xrpl.xrpl4j.model.client.ledger.LedgerRequestParams;
|
||||
import org.xrpl.xrpl4j.model.client.transactions.SubmitResult;
|
||||
import org.xrpl.xrpl4j.model.client.transactions.TransactionRequestParams;
|
||||
import org.xrpl.xrpl4j.model.client.transactions.TransactionResult;
|
||||
import org.xrpl.xrpl4j.model.jackson.ObjectMapperFactory;
|
||||
import org.xrpl.xrpl4j.model.transactions.Address;
|
||||
import org.xrpl.xrpl4j.model.transactions.CredentialAccept;
|
||||
import org.xrpl.xrpl4j.model.transactions.CredentialCreate;
|
||||
import org.xrpl.xrpl4j.model.transactions.CredentialDelete;
|
||||
import org.xrpl.xrpl4j.model.transactions.CredentialType;
|
||||
import org.xrpl.xrpl4j.model.transactions.Transaction;
|
||||
import org.xrpl.xrpl4j.model.transactions.TransactionResultCodes;
|
||||
import org.xrpl.xrpl4j.model.transactions.XrpCurrencyAmount;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
/**
|
||||
* This code sample demonstrates the Credential lifecycle on the XRPL.
|
||||
* It issues a credential to a subject, accepts the credential, and then deletes it.
|
||||
*/
|
||||
public class ManageCredentials {
|
||||
|
||||
private static final HttpUrl NETWORK_URL = HttpUrl.get("https://s.altnet.rippletest.net:51234/");
|
||||
private static final HttpUrl FAUCET_URL = HttpUrl.get("https://faucet.altnet.rippletest.net");
|
||||
private static final String EXPLORER_BASE = "https://testnet.xrpl.org/transactions/";
|
||||
|
||||
private static final CredentialType CREDENTIAL_TYPE = CredentialType.ofPlainText("kyc-trader");
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
run();
|
||||
} catch (Exception e) {
|
||||
// Unwrap CompletionException so async failures print the same clean message
|
||||
// as sync failures. CompletableFuture.join() wraps exceptions in CompletionException
|
||||
Throwable cause = (e instanceof CompletionException && e.getCause() != null)
|
||||
? e.getCause() : e;
|
||||
System.err.println("Error: " + cause.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void run() {
|
||||
|
||||
// ----- Connect to Testnet and fund accounts -----
|
||||
XrplClient xrplClient = new XrplClient(NETWORK_URL);
|
||||
System.out.println("\n=== Funding issuer and subject accounts on Testnet ===\n");
|
||||
|
||||
CompletableFuture<KeyPair> issuerFuture = CompletableFuture.supplyAsync(
|
||||
() -> createAndFundWallet(xrplClient));
|
||||
CompletableFuture<KeyPair> subjectFuture = CompletableFuture.supplyAsync(
|
||||
() -> createAndFundWallet(xrplClient));
|
||||
CompletableFuture.allOf(issuerFuture, subjectFuture).join();
|
||||
|
||||
KeyPair issuer = issuerFuture.join();
|
||||
KeyPair subject = subjectFuture.join();
|
||||
Address issuerAddress = issuer.publicKey().deriveAddress();
|
||||
Address subjectAddress = subject.publicKey().deriveAddress();
|
||||
System.out.println("Issuer: " + issuerAddress);
|
||||
System.out.println("Subject: " + subjectAddress);
|
||||
|
||||
// ----- Prepare CredentialCreate transaction -----
|
||||
System.out.println("\n=== Preparing CredentialCreate transaction ===\n");
|
||||
|
||||
CredentialCreate createTx = CredentialCreate.builder()
|
||||
.account(issuerAddress)
|
||||
.subject(subjectAddress)
|
||||
.credentialType(CREDENTIAL_TYPE)
|
||||
.sequence(accountSequence(xrplClient, issuerAddress))
|
||||
.fee(recommendedFee(xrplClient))
|
||||
.lastLedgerSequence(lastLedgerSequence(xrplClient))
|
||||
.signingPublicKey(issuer.publicKey())
|
||||
.build();
|
||||
printTransactionJson(createTx);
|
||||
|
||||
// ----- Sign, submit, and wait for CredentialCreate validation -----
|
||||
System.out.println("\n=== Submitting CredentialCreate transaction ===\n");
|
||||
|
||||
TransactionResult<CredentialCreate> createResult = signSubmitAndWait(
|
||||
xrplClient, issuer, createTx, CredentialCreate.class);
|
||||
|
||||
requireSuccess(createResult);
|
||||
|
||||
// ----- Prepare CredentialAccept transaction -----
|
||||
System.out.println("\n=== Preparing CredentialAccept transaction ===\n");
|
||||
|
||||
CredentialAccept acceptTx = CredentialAccept.builder()
|
||||
.account(subjectAddress)
|
||||
.issuer(issuerAddress)
|
||||
.credentialType(CREDENTIAL_TYPE)
|
||||
.sequence(accountSequence(xrplClient, subjectAddress))
|
||||
.fee(recommendedFee(xrplClient))
|
||||
.lastLedgerSequence(lastLedgerSequence(xrplClient))
|
||||
.signingPublicKey(subject.publicKey())
|
||||
.build();
|
||||
printTransactionJson(acceptTx);
|
||||
|
||||
// ----- Sign, Submit, and wait for CredentialAccept validation -----
|
||||
System.out.println("\n=== Submitting CredentialAccept transaction ===\n");
|
||||
|
||||
TransactionResult<CredentialAccept> acceptResult = signSubmitAndWait(
|
||||
xrplClient, subject, acceptTx, CredentialAccept.class);
|
||||
|
||||
requireSuccess(acceptResult);
|
||||
|
||||
// ----- Prepare CredentialDelete transaction -----
|
||||
System.out.println("\n=== Preparing CredentialDelete transaction ===\n");
|
||||
|
||||
CredentialDelete deleteTx = CredentialDelete.builder()
|
||||
.account(subjectAddress)
|
||||
.issuer(issuerAddress)
|
||||
.credentialType(CREDENTIAL_TYPE)
|
||||
.sequence(accountSequence(xrplClient, subjectAddress))
|
||||
.fee(recommendedFee(xrplClient))
|
||||
.lastLedgerSequence(lastLedgerSequence(xrplClient))
|
||||
.signingPublicKey(subject.publicKey())
|
||||
.build();
|
||||
printTransactionJson(deleteTx);
|
||||
|
||||
// ----- Sign, Submit, and wait for CredentialDelete validation -----
|
||||
System.out.println("\n=== Submitting CredentialDelete transaction ===\n");
|
||||
|
||||
TransactionResult<CredentialDelete> deleteResult = signSubmitAndWait(
|
||||
xrplClient, subject, deleteTx, CredentialDelete.class);
|
||||
|
||||
requireSuccess(deleteResult);
|
||||
}
|
||||
|
||||
// ===== Helper functions =====
|
||||
|
||||
// Generates a new Ed25519 keypair, funds it from the Testnet faucet, and
|
||||
// returns the keypair once the account is visible on a validated ledger.
|
||||
private static KeyPair createAndFundWallet(XrplClient xrplClient) {
|
||||
KeyPair keyPair = Seed.ed25519Seed().deriveKeyPair();
|
||||
Address address = keyPair.publicKey().deriveAddress();
|
||||
FaucetClient faucetClient = FaucetClient.construct(FAUCET_URL);
|
||||
faucetClient.fundAccount(FundAccountRequest.of(address));
|
||||
|
||||
for (int attempt = 0; attempt < 20; attempt++) {
|
||||
try {
|
||||
xrplClient.accountInfo(AccountInfoRequestParams.builder()
|
||||
.account(address)
|
||||
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
|
||||
.build());
|
||||
return keyPair;
|
||||
} catch (JsonRpcClientErrorException notYetVisible) {
|
||||
try {
|
||||
Thread.sleep(1_000L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Account polling interrupted for " + address + ". " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Faucet funding for " + address + " did not confirm in time.");
|
||||
}
|
||||
|
||||
// Fetches the next transaction sequence number of an address from
|
||||
// the latest validated ledger.
|
||||
private static UnsignedInteger accountSequence(XrplClient xrplClient, Address address) {
|
||||
try {
|
||||
AccountInfoResult info = xrplClient.accountInfo(AccountInfoRequestParams.builder()
|
||||
.account(address)
|
||||
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
|
||||
.build());
|
||||
return info.accountData().sequence();
|
||||
} catch (JsonRpcClientErrorException e) {
|
||||
throw new RuntimeException("Failed to fetch account sequence for " + address + ". " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetches the current network fee and returns the recommended fee for
|
||||
// a standard (non-multisig, non-batch) transaction.
|
||||
private static XrpCurrencyAmount recommendedFee(XrplClient xrplClient) {
|
||||
try {
|
||||
return FeeUtils.computeNetworkFees(xrplClient.fee()).recommendedFee();
|
||||
} catch (JsonRpcClientErrorException e) {
|
||||
throw new RuntimeException("Failed to fetch network fee. " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Computes a safe LastLedgerSequence for a new transaction. The
|
||||
// latest validated ledger index plus a small buffer (20 ledgers).
|
||||
private static UnsignedInteger lastLedgerSequence(XrplClient xrplClient) {
|
||||
try {
|
||||
UnsignedInteger validatedLedger = xrplClient.ledger(LedgerRequestParams.builder()
|
||||
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
|
||||
.build())
|
||||
.ledgerIndexSafe()
|
||||
.unsignedIntegerValue();
|
||||
return validatedLedger.plus(UnsignedInteger.valueOf(20));
|
||||
} catch (JsonRpcClientErrorException e) {
|
||||
throw new RuntimeException("Failed to compute LastLedgerSequence. " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Prints a transaction as a formatted JSON.
|
||||
private static void printTransactionJson(Transaction tx) {
|
||||
try {
|
||||
System.out.println(ObjectMapperFactory.create().writerWithDefaultPrettyPrinter().writeValueAsString(tx));
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to serialize transaction JSON. " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Signs and submits a transaction, then polls the network until
|
||||
// the transaction reaches a validated state.
|
||||
private static <T extends Transaction> TransactionResult<T> signSubmitAndWait(
|
||||
XrplClient xrplClient,
|
||||
KeyPair signer,
|
||||
T transaction,
|
||||
Class<T> transactionType
|
||||
) {
|
||||
SignatureService<PrivateKey> signatureService = new BcSignatureService();
|
||||
|
||||
UnsignedInteger lastLedgerSequence = transaction.lastLedgerSequence()
|
||||
.orElseThrow(() -> new IllegalArgumentException(
|
||||
"Must set LastLedgerSequence for polling expiration"));
|
||||
|
||||
try {
|
||||
SingleSignedTransaction<T> signed = signatureService.sign(signer.privateKey(), transaction);
|
||||
SubmitResult<T> submit = xrplClient.submit(signed);
|
||||
|
||||
if (!TransactionResultCodes.TES_SUCCESS.equals(submit.engineResult())) {
|
||||
throw new IllegalStateException(
|
||||
"Submission rejected. " + submit.engineResult() + " — " + submit.engineResultMessage());
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(1_000L);
|
||||
|
||||
// Poll network for validated status using tx hash
|
||||
try {
|
||||
TransactionResult<T> result = xrplClient.transaction(
|
||||
TransactionRequestParams.of(signed.hash()), transactionType);
|
||||
if (result.validated()) {
|
||||
return result;
|
||||
}
|
||||
} catch (JsonRpcClientErrorException e) {
|
||||
// Transaction not found; keep polling.
|
||||
}
|
||||
|
||||
// Check if transaction expired before polling again
|
||||
UnsignedInteger currentLedger = xrplClient.ledger(LedgerRequestParams.builder()
|
||||
.ledgerSpecifier(LedgerSpecifier.VALIDATED)
|
||||
.build())
|
||||
.ledgerIndexSafe()
|
||||
.unsignedIntegerValue();
|
||||
if (currentLedger.compareTo(lastLedgerSequence) > 0) {
|
||||
throw new IllegalStateException("Transaction expired. Current ledger " + currentLedger
|
||||
+ " passed LastLedgerSequence " + lastLedgerSequence);
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("Transaction polling interrupted. " + e.getMessage(), e);
|
||||
} catch (JsonRpcClientErrorException | JsonProcessingException e) {
|
||||
throw new RuntimeException("Transaction processing failed. " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for a tesSUCCESS result code. If true, prints an explorer
|
||||
// link. Otherwise, throws an error.
|
||||
private static void requireSuccess(TransactionResult<?> result) {
|
||||
String code = result.metadata().get().transactionResult();
|
||||
String txType = result.transaction().transactionType().value();
|
||||
if (!TransactionResultCodes.TES_SUCCESS.equals(code)) {
|
||||
throw new IllegalStateException(txType + " failed with error code " + code);
|
||||
}
|
||||
System.out.println(txType + " succeeded!");
|
||||
System.out.println("Explorer: " + EXPLORER_BASE + result.hash());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Quiets xrpl4j's DEBUG chatter so tutorial output stays readable.
|
||||
Raise xrpl4j to DEBUG to see wire-level transaction details. -->
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.xrpl.xrpl4j" level="WARN"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
@@ -373,7 +373,6 @@
|
||||
[get_counts command]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
|
||||
[get_counts method]: /docs/references/http-websocket-apis/admin-api-methods/status-and-debugging-methods/get_counts.md
|
||||
[Get Started Using Go]: /docs/tutorials/get-started/get-started-go.md
|
||||
[Get Started Using Java]: /docs/tutorials/get-started/get-started-java.md
|
||||
[Get Started Using JavaScript]: /docs/tutorials/get-started/get-started-javascript.md
|
||||
[Get Started Using Python]: /docs/tutorials/get-started/get-started-python.md
|
||||
[hexadecimal]: https://en.wikipedia.org/wiki/Hexadecimal
|
||||
@@ -491,7 +490,6 @@
|
||||
[vault_info method]: /docs/references/http-websocket-apis/public-api-methods/vault-methods/vault_info.md
|
||||
[wallet_propose command]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
|
||||
[wallet_propose method]: /docs/references/http-websocket-apis/admin-api-methods/key-generation-methods/wallet_propose.md
|
||||
[xrpl4j library]: https://github.com/XRPLF/xrpl4j
|
||||
[xrpl-go library]: https://github.com/XRPLF/xrpl-go
|
||||
[xrpl.js library]: https://github.com/XRPLF/xrpl.js
|
||||
[xrpl-py library]: https://github.com/XRPLF/xrpl-py
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
---
|
||||
seo:
|
||||
description: Issue, accept, and delete a credential on the XRP Ledger.
|
||||
metadata:
|
||||
indexPage: true
|
||||
labels:
|
||||
- Credentials
|
||||
---
|
||||
|
||||
# Manage Credentials
|
||||
|
||||
This tutorial shows you how to manage the full lifecycle of [Credentials][] on the XRP Ledger: issuing a credential to a subject, accepting the credential, and deleting it.
|
||||
|
||||
{% amendment-disclaimer name="Credentials" /%}
|
||||
|
||||
## Goals
|
||||
|
||||
By the end of this tutorial, you will be able to:
|
||||
|
||||
- Issue a credential to a subject account.
|
||||
- Accept a credential as the subject.
|
||||
- Delete a credential from the ledger.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To complete this tutorial, you should:
|
||||
|
||||
- Have a basic understanding of the XRP Ledger.
|
||||
- Have an XRP Ledger client library set up in your development environment. This page provides examples for the following:
|
||||
- **Java** with the [xrpl4j library][]. See [Get Started Using Java][] for setup steps.
|
||||
|
||||
## Source Code
|
||||
|
||||
You can find the complete source code for this tutorial's examples in the {% repo-link path="_code-samples/credential/" %}code samples section of this website's repository{% /repo-link %}.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Install dependencies
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
From the code sample folder, use `mvn` to install dependencies.
|
||||
|
||||
```bash
|
||||
mvn install
|
||||
```
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 2. Set up client and fund accounts
|
||||
|
||||
To get started, import the necessary libraries and instantiate a client to connect to the XRPL Testnet. This example imports:
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
- `xrpl4j`: Used for XRPL client connection, transaction submission, and wallet handling.
|
||||
- `OkHttp`, `Guava`, `Jackson`: Used for HTTP URL construction, unsigned integer arithmetic, and JSON serialization.
|
||||
- `java.util.concurrent`: Used for async operations.
|
||||
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" before="// ----- Prepare CredentialCreate" /%}
|
||||
|
||||
The `createAndFundWallet()` helper generates an Ed25519 keypair, funds it from the Testnet faucet, and polls Testnet until the account is visible on a validated ledger.
|
||||
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Generates a new Ed25519 keypair" before="// Fetches the next transaction sequence number" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 3. Prepare CredentialCreate transaction
|
||||
|
||||
Create the [CredentialCreate transaction][] object.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialCreate" before="// ----- Sign, submit, and wait for CredentialCreate" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
The credential is identified by the issuer, subject, and credential type (written as a hexadecimal string).
|
||||
|
||||
### 4. Submit CredentialCreate transaction
|
||||
|
||||
Sign and submit the `CredentialCreate` transaction to the XRP Ledger.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, submit, and wait for CredentialCreate" before="// ----- Prepare CredentialAccept" /%}
|
||||
|
||||
The `signSubmitAndWait()` helper signs a transaction, submits it, and polls Testnet until it reaches a validated ledger.
|
||||
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Signs and submits a transaction" before="// Checks for a tesSUCCESS result code" /%}
|
||||
|
||||
The `requireSuccess` helper verifies that the transaction succeeded with a `tesSUCCESS` result code and posts a link to the transaction metadata on the XRPL Explorer.
|
||||
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// Checks for a tesSUCCESS result code" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 5. Prepare CredentialAccept transaction
|
||||
|
||||
Create the [CredentialAccept transaction][] object. The subject account must accept the credential to make it valid.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialAccept" before="// ----- Sign, Submit, and wait for CredentialAccept" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 6. Submit CredentialAccept transaction
|
||||
|
||||
Sign and submit the `CredentialAccept` transaction to the XRP Ledger.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, Submit, and wait for CredentialAccept" before="// ----- Prepare CredentialDelete" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 7. Prepare CredentialDelete transaction
|
||||
|
||||
Create the [CredentialDelete transaction][] object. Either the issuer or the subject can delete a credential.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Prepare CredentialDelete" before="// ----- Sign, Submit, and wait for CredentialDelete" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
### 8. Submit CredentialDelete transaction
|
||||
|
||||
Sign and submit the `CredentialDelete` transaction to the XRP Ledger.
|
||||
|
||||
{% tabs %}
|
||||
{% tab label="Java" %}
|
||||
{% code-snippet file="/_code-samples/credential/java/src/main/java/com/example/xrpl/ManageCredentials.java" language="java" from="// ----- Sign, Submit, and wait for CredentialDelete" before="// ===== Helper functions" /%}
|
||||
{% /tab %}
|
||||
{% /tabs %}
|
||||
|
||||
## See Also
|
||||
|
||||
**Concepts**:
|
||||
- [Credentials][]
|
||||
|
||||
**Tutorials**:
|
||||
- [Verify Credentials](./verify-credentials.md)
|
||||
|
||||
**References**:
|
||||
- [CredentialCreate transaction][]
|
||||
- [CredentialAccept transaction][]
|
||||
- [CredentialDelete transaction][]
|
||||
|
||||
{% raw-partial file="/docs/_snippets/common-links.md" /%}
|
||||
@@ -133,6 +133,13 @@ analytics:
|
||||
defaultDataLayer:
|
||||
platform: redocly
|
||||
enableWebVitalsTracking: true
|
||||
feedback:
|
||||
type: scale
|
||||
settings:
|
||||
label: How helpful was this page?
|
||||
submitText: Thanks for your feedback!
|
||||
leftScaleLabel: Not helpful
|
||||
rightScaleLabel: Very helpful
|
||||
scripts:
|
||||
head:
|
||||
- src: https://cmp.osano.com/AzyjT6TIZMlgyLyy8/f11f7772-8ed5-4b73-bd17-c0814edcc440/osano.js
|
||||
|
||||
@@ -23,7 +23,6 @@ The image files used in blog posts are located in the `blog/img` directory.
|
||||
|
||||
The blog posts are grouped by year, so all blog posts published in year 2025 are located in the `blog/2025` directory.
|
||||
|
||||
|
||||
## Steps to Create a New Blog Post
|
||||
|
||||
To create a new post, follow these steps:
|
||||
@@ -40,43 +39,4 @@ To create a new post, follow these steps:
|
||||
|
||||
6. When the draft is ready for review, save and commit your updates.
|
||||
|
||||
7. Create a new PR to merge your changes to master.
|
||||
|
||||
|
||||
### Release Notes
|
||||
|
||||
We have streamlined the release notes process with the assistance of **Claude** skills. To create new release notes, follow these steps:
|
||||
|
||||
1. Load the `generate-release-notes` skill. **Claude** _should_ auto-load it when you set the working directory to the `xrpl-dev-portal` repository. If not, you can explicitly point to the skill located at `.claude/skills/generate-release-notes/SKILL.md`.
|
||||
{% admonition type="info" name="Note" %}Although the skill is optimized for **Claude**, you can give this `SKILL.md` file to any coding agent.{% /admonition %}
|
||||
|
||||
2. Run the `generate-release-notes` skill.
|
||||
```bash
|
||||
/generate-release-notes --from <branch-or-tag> --to <branch-or-tag>
|
||||
```
|
||||
|
||||
The skill accepts these arguments:
|
||||
|
||||
| Arguments | Description |
|
||||
|:-----------|:------------|
|
||||
| `--from` | (required) Base ref. Must match the exact [tag or branch](https://github.com/XRPLF/rippled/branches) the current version of `rippled` is building from. |
|
||||
| `--to` | (required) Target ref. Must match the exact [tag or branch](https://github.com/XRPLF/rippled/branches) the upcoming version of `rippled` will be built from. |
|
||||
| `--date` | (optional) Release date in `YYYY-MM-DD` format. Defaults to current date. |
|
||||
| `--output` | (optional) Output file path. Defaults to `blog/<year>/rippled-<version>.md`. |
|
||||
|
||||
3. The AI executes these steps:
|
||||
- Runs a Python script that fetches all commits and PR details between the two refs and outputs a draft blog post.
|
||||
- Processes all amendment-related changes, keeping, removing, or merging entries based on which amendments are part of the release.
|
||||
- Sorts remaining entries into subsections (Features, Breaking Changes, Bug Fixes, etc.) based on files touched, PR labels, and descriptions.
|
||||
- Reformats each entry to match the release notes style and writes a summary of the release.
|
||||
- Cleans up empty sections and adds the post to the sidebar.
|
||||
|
||||
{% admonition type="info" name="Note" %}Depending on the size of the release, this process can take upwards of ten minutes.{% /admonition %}
|
||||
|
||||
4. Review the final output.
|
||||
- Check the descriptions of any entries in **Amendments**, **Features**, or **Breaking Changes**.
|
||||
- Verify the integrity of the package links and add in the SHA-256 hashes.
|
||||
- Verify the commit linked in the **Install / Upgrade** section points to the version bump commit.
|
||||
- If you didn't pass in the correct release date using the `--date` argument, update the `date` field in the frontmatter.
|
||||
|
||||
5. Create a new PR to merge the release notes to `master`.
|
||||
7. Create a new PR to merge your changes to master.
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
expanded: false
|
||||
items:
|
||||
- page: docs/tutorials/compliance-features/require-destination-tags.md
|
||||
- page: docs/tutorials/compliance-features/manage-credentials.md
|
||||
- page: docs/tutorials/compliance-features/verify-credentials.md
|
||||
- page: docs/tutorials/compliance-features/create-permissioned-domains-in-javascript.md
|
||||
- group: Programmability
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
||||
|
||||
[data-component-name="Markdown/Markdown"] article {
|
||||
padding-bottom: 50px;
|
||||
border-bottom: 1px solid $gray-400;
|
||||
|
||||
p code,
|
||||
table code,
|
||||
|
||||
@@ -1,96 +1,3 @@
|
||||
// TEMPORARY: overriding the feedback widget styles here
|
||||
#feedback-content {
|
||||
.docked-widget {
|
||||
border: none !important;
|
||||
background-color: transparent !important;
|
||||
position: static !important;
|
||||
box-shadow: none !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.widget-form-wrapper {
|
||||
position: static !important;
|
||||
box-shadow: none !important;
|
||||
display: block;
|
||||
background-color: $gray-800 !important;
|
||||
border-width: 0 !important;
|
||||
padding: 24px !important;
|
||||
border-radius: 8px !important;
|
||||
|
||||
div {
|
||||
background-color: $gray-800 !important;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: $white !important;
|
||||
opacity: 1 !important;
|
||||
border: none !important;
|
||||
border-radius: 4px !important;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
color: $black !important;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
background: none !important;
|
||||
flex-grow: 0 !important;
|
||||
padding-right: 1rem !important;
|
||||
height: auto !important;
|
||||
padding: 0 !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
.widget-header-footer {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.widget-form-footer {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.submit {
|
||||
background-color: $blue-purple-500 !important;
|
||||
font-weight: bold !important;
|
||||
color: $white !important;
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
border-radius: 4px !important;
|
||||
margin: 0 !important;
|
||||
margin-top: 8px !important;
|
||||
&:hover {
|
||||
background: $blue-purple-600 !important;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled="disabled"] {
|
||||
background-color: $blue-purple-700 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: $blue-purple-700 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin: 0 !important;
|
||||
margin-top: 8px !important;
|
||||
color: $blue-purple-300 !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
}
|
||||
|
||||
#closeFeedback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.widget-helpful {
|
||||
.widget-header {
|
||||
background-color: $gray-800 !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
color: $white !important;
|
||||
}
|
||||
}
|
||||
[data-component-name="Feedback/Feedback"] {
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
@@ -1120,6 +1120,10 @@ main article .card-grid {
|
||||
}
|
||||
}
|
||||
|
||||
[data-component-name="Markdown/Markdown"] article {
|
||||
border-bottom-color: $gray-500;
|
||||
}
|
||||
|
||||
.dev-blog {
|
||||
.text-bg {
|
||||
background-color: $white;
|
||||
|
||||
@@ -1,697 +0,0 @@
|
||||
"""
|
||||
Generate rippled release notes from GitHub commit history.
|
||||
|
||||
Usage (from repo root):
|
||||
python3 tools/generate-release-notes.py --from release-3.0 --to release-3.1 [--date 2026-03-24] [--output path/to/file.md]
|
||||
|
||||
Arguments:
|
||||
--from (required) Base ref — must match exact tag or branch to compare from.
|
||||
--to (required) Target ref — must match exact tag or branch to compare to.
|
||||
--date (optional) Release date in YYYY-MM-DD format. Defaults to today.
|
||||
--output (optional) Output file path. Defaults to blog/<year>/rippled-<version>.md.
|
||||
|
||||
Requires: gh CLI (authenticated)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
# Emails to exclude from credits (Ripple employees not using @ripple.com).
|
||||
# Commits from @ripple.com addresses are already filtered automatically.
|
||||
EXCLUDED_EMAILS = {
|
||||
"3maisons@gmail.com", # Luc des Trois Maisons
|
||||
"a1q123456@users.noreply.github.com", # Jingchen Wu
|
||||
"bthomee@users.noreply.github.com", # Bart Thomee
|
||||
"21219765+ckeshava@users.noreply.github.com", # Chenna Keshava B S
|
||||
"gregtatcam@users.noreply.github.com", # Gregory Tsipenyuk
|
||||
"kuzzz99@gmail.com", # Sergey Kuznetsov
|
||||
"legleux@users.noreply.github.com", # Michael Legleux
|
||||
"mathbunnyru@users.noreply.github.com", # Ayaz Salikhov
|
||||
"mvadari@gmail.com", # Mayukha Vadari
|
||||
"115580134+oleks-rip@users.noreply.github.com", # Oleksandr Pidskopnyi
|
||||
"3397372+pratikmankawde@users.noreply.github.com", # Pratik Mankawde
|
||||
"35279399+shawnxie999@users.noreply.github.com", # Shawn Xie
|
||||
"5780819+Tapanito@users.noreply.github.com", # Vito Tumas
|
||||
"13349202+vlntb@users.noreply.github.com", # Valentin Balaschenko
|
||||
"129996061+vvysokikh1@users.noreply.github.com", # Vladislav Vysokikh
|
||||
"vvysokikh@gmail.com", # Vladislav Vysokikh
|
||||
}
|
||||
|
||||
|
||||
# Pre-compiled patterns for skipping version commits
|
||||
SKIP_PATTERNS = [
|
||||
re.compile(r"^Set version to", re.IGNORECASE),
|
||||
re.compile(r"^Version \d", re.IGNORECASE),
|
||||
re.compile(r"bump version to", re.IGNORECASE),
|
||||
re.compile(r"^Merge tag ", re.IGNORECASE),
|
||||
]
|
||||
|
||||
|
||||
# --- API helpers ---
|
||||
|
||||
def run_gh_rest(endpoint):
|
||||
"""Run a gh api REST command and return parsed JSON."""
|
||||
result = subprocess.run(
|
||||
["gh", "api", endpoint],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"Error running gh api: {result.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def run_gh_graphql(query):
|
||||
"""Run a gh api graphql command and return parsed JSON.
|
||||
Handles partial failures (e.g., missing PRs) by returning
|
||||
whatever data is available alongside errors.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["gh", "api", "graphql", "-f", f"query={query}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
return json.loads(result.stdout)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
print(f"Error running graphql: {result.stderr}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def fetch_commit_files(sha):
|
||||
"""Fetch list of files changed in a commit via REST API.
|
||||
Returns empty list on failure instead of exiting.
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["gh", "api", f"repos/XRPLF/rippled/commits/{sha}"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" Warning: Could not fetch files for commit {sha[:7]}", file=sys.stderr)
|
||||
return []
|
||||
data = json.loads(result.stdout)
|
||||
return [f["filename"] for f in data.get("files", [])]
|
||||
|
||||
|
||||
# --- Data fetching ---
|
||||
|
||||
def fetch_version_info(ref):
|
||||
"""Fetch version string and version-setting commit info in a single GraphQL call.
|
||||
Returns (version_string, formatted_commit_block).
|
||||
"""
|
||||
data = run_gh_graphql(f"""
|
||||
{{
|
||||
repository(owner: "XRPLF", name: "rippled") {{
|
||||
file: object(expression: "{ref}:src/libxrpl/protocol/BuildInfo.cpp") {{
|
||||
... on Blob {{ text }}
|
||||
}}
|
||||
ref: object(expression: "{ref}") {{
|
||||
... on Commit {{
|
||||
history(first: 1, path: "src/libxrpl/protocol/BuildInfo.cpp") {{
|
||||
nodes {{
|
||||
oid
|
||||
message
|
||||
author {{
|
||||
name
|
||||
email
|
||||
date
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
repo = data.get("data", {}).get("repository", {})
|
||||
|
||||
# Extract version string from BuildInfo.cpp
|
||||
file_text = (repo.get("file") or {}).get("text", "")
|
||||
match = re.search(r'versionString\s*=\s*"([^"]+)"', file_text)
|
||||
if not match:
|
||||
print("Warning: Could not find versionString in BuildInfo.cpp. Using placeholder.", file=sys.stderr)
|
||||
version = match.group(1) if match else "TODO"
|
||||
|
||||
# Extract version commit info
|
||||
nodes = (repo.get("ref") or {}).get("history", {}).get("nodes", [])
|
||||
if not nodes:
|
||||
commit_block = "commit TODO\nAuthor: TODO\nDate: TODO\n\n Set version to TODO"
|
||||
else:
|
||||
commit = nodes[0]
|
||||
raw_date = commit["author"]["date"]
|
||||
try:
|
||||
dt = datetime.fromisoformat(raw_date)
|
||||
formatted_date = dt.strftime("%a %b %-d %H:%M:%S %Y %z")
|
||||
except ValueError:
|
||||
formatted_date = raw_date
|
||||
|
||||
name = commit["author"]["name"]
|
||||
email = commit["author"]["email"]
|
||||
sha = commit["oid"]
|
||||
message = commit["message"].split("\n")[0]
|
||||
commit_block = f"commit {sha}\nAuthor: {name} <{email}>\nDate: {formatted_date}\n\n {message}"
|
||||
|
||||
return version, commit_block
|
||||
|
||||
|
||||
def fetch_commits(from_ref, to_ref):
|
||||
"""Fetch all commits between two refs using the GitHub compare API."""
|
||||
commits = []
|
||||
page = 1
|
||||
while True:
|
||||
data = run_gh_rest(
|
||||
f"repos/XRPLF/rippled/compare/{from_ref}...{to_ref}?per_page=250&page={page}"
|
||||
)
|
||||
batch = data.get("commits", [])
|
||||
commits.extend(batch)
|
||||
if len(batch) < 250:
|
||||
break
|
||||
page += 1
|
||||
return commits
|
||||
|
||||
|
||||
def parse_features_macro(text):
|
||||
"""Parse features.macro into {amendment_name: status_string} dict."""
|
||||
results = {}
|
||||
for match in re.finditer(
|
||||
r'XRPL_(FEATURE|FIX)\s*\(\s*(\w+)\s*,\s*Supported::(\w+)\s*,\s*VoteBehavior::(\w+)', text):
|
||||
macro_type, name, supported, vote = match.groups()
|
||||
key = f"fix{name}" if macro_type == "FIX" else name
|
||||
results[key] = f"{supported}, {vote}"
|
||||
for match in re.finditer(r'XRPL_RETIRE(?:_(FEATURE|FIX))?\s*\(\s*(\w+)\s*\)', text):
|
||||
macro_type, name = match.groups()
|
||||
key = f"fix{name}" if macro_type == "FIX" else name
|
||||
results[key] = "retired"
|
||||
return results
|
||||
|
||||
|
||||
def fetch_amendment_diff(from_ref, to_ref):
|
||||
"""Compare features.macro between two refs to find amendment changes.
|
||||
Returns (changes, unchanged) where:
|
||||
- changes: {name: True/False} for amendments that changed status
|
||||
- unchanged: {name: True/False} for amendments with no status change
|
||||
True = include; False = exclude
|
||||
"""
|
||||
macro_path = "repos/XRPLF/rippled/contents/include/xrpl/protocol/detail/features.macro"
|
||||
|
||||
from_data = run_gh_rest(f"{macro_path}?ref={from_ref}")
|
||||
from_text = base64.b64decode(from_data["content"]).decode()
|
||||
from_amendments = parse_features_macro(from_text)
|
||||
|
||||
to_data = run_gh_rest(f"{macro_path}?ref={to_ref}")
|
||||
to_text = base64.b64decode(to_data["content"]).decode()
|
||||
to_amendments = parse_features_macro(to_text)
|
||||
|
||||
changes = {}
|
||||
for name, to_status in to_amendments.items():
|
||||
if name not in from_amendments:
|
||||
# New amendment — include only if Supported::yes
|
||||
changes[name] = to_status.startswith("yes")
|
||||
elif from_amendments[name] != to_status:
|
||||
# Include if either old or new status involves yes (voting-ready)
|
||||
from_status = from_amendments[name]
|
||||
changes[name] = from_status.startswith("yes") or to_status.startswith("yes")
|
||||
|
||||
# Removed amendments — include only if they were Supported::yes
|
||||
for name in from_amendments:
|
||||
if name not in to_amendments:
|
||||
changes[name] = from_amendments[name].startswith("yes")
|
||||
|
||||
# Unchanged amendments to also exclude (unreleased work)
|
||||
unchanged = sorted(
|
||||
name for name, to_status in to_amendments.items()
|
||||
if name not in changes and to_status != "retired" and not to_status.startswith("yes")
|
||||
)
|
||||
|
||||
return changes, unchanged
|
||||
|
||||
|
||||
def fetch_prs_graphql(pr_numbers):
|
||||
"""Fetch PR details in batches using GitHub GraphQL API.
|
||||
Falls back to issue lookup for numbers that aren't PRs.
|
||||
Returns a dict of {number: {title, body, labels, files, type}}.
|
||||
"""
|
||||
results = {}
|
||||
missing = []
|
||||
batch_size = 50
|
||||
pr_list = list(pr_numbers)
|
||||
|
||||
# Fetch PRs
|
||||
for i in range(0, len(pr_list), batch_size):
|
||||
batch = pr_list[i:i + batch_size]
|
||||
|
||||
fragments = []
|
||||
for pr_num in batch:
|
||||
fragments.append(f"""
|
||||
pr{pr_num}: pullRequest(number: {pr_num}) {{
|
||||
title
|
||||
body
|
||||
labels(first: 10) {{
|
||||
nodes {{ name }}
|
||||
}}
|
||||
files(first: 100) {{
|
||||
nodes {{ path }}
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
query = f"""
|
||||
{{
|
||||
repository(owner: "XRPLF", name: "rippled") {{
|
||||
{"".join(fragments)}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
data = run_gh_graphql(query)
|
||||
repo_data = data.get("data", {}).get("repository", {})
|
||||
|
||||
for alias, pr_data in repo_data.items():
|
||||
pr_num = int(alias.removeprefix("pr"))
|
||||
if pr_data:
|
||||
results[pr_num] = {
|
||||
"title": pr_data["title"],
|
||||
"body": clean_pr_body(pr_data.get("body") or ""),
|
||||
"labels": [l["name"] for l in pr_data.get("labels", {}).get("nodes", [])],
|
||||
"files": [f["path"] for f in pr_data.get("files", {}).get("nodes", [])],
|
||||
"type": "pull",
|
||||
}
|
||||
else:
|
||||
missing.append(pr_num)
|
||||
|
||||
print(f" Fetched {min(i + batch_size, len(pr_list))}/{len(pr_list)} PRs...")
|
||||
|
||||
# Fetch missing numbers as issues
|
||||
if missing:
|
||||
print(f" Looking up {len(missing)} missing PR numbers as Issues...")
|
||||
for i in range(0, len(missing), batch_size):
|
||||
batch = missing[i:i + batch_size]
|
||||
|
||||
fragments = []
|
||||
for num in batch:
|
||||
fragments.append(f"""
|
||||
issue{num}: issue(number: {num}) {{
|
||||
title
|
||||
body
|
||||
labels(first: 10) {{
|
||||
nodes {{ name }}
|
||||
}}
|
||||
}}
|
||||
""")
|
||||
|
||||
query = f"""
|
||||
{{
|
||||
repository(owner: "XRPLF", name: "rippled") {{
|
||||
{"".join(fragments)}
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
data = run_gh_graphql(query)
|
||||
repo_data = data.get("data", {}).get("repository", {})
|
||||
|
||||
for alias, issue_data in repo_data.items():
|
||||
if issue_data:
|
||||
num = int(alias.removeprefix("issue"))
|
||||
results[num] = {
|
||||
"title": issue_data["title"],
|
||||
"body": clean_pr_body(issue_data.get("body") or ""),
|
||||
"labels": [l["name"] for l in issue_data.get("labels", {}).get("nodes", [])],
|
||||
"type": "issues",
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# --- Utilities ---
|
||||
|
||||
def clean_pr_body(text):
|
||||
"""Strip HTML comments and PR template boilerplate from body text."""
|
||||
# Remove HTML comments
|
||||
text = re.sub(r"<!--.*?-->", "", text, flags=re.DOTALL)
|
||||
# Remove unchecked checkbox lines, keep checked ones
|
||||
text = re.sub(r"^- \[ \] .+$", "", text, flags=re.MULTILINE)
|
||||
# Remove all markdown headings
|
||||
text = re.sub(r"^#{1,6} .+$", "", text, flags=re.MULTILINE)
|
||||
# Convert bare GitHub URLs to markdown links
|
||||
text = re.sub(r"(?<!\()https://github\.com/XRPLF/rippled/(pull|issues)/(\d+)(#[^\s)]*)?", r"[#\2](https://github.com/XRPLF/rippled/\1/\2\3)", text)
|
||||
# Convert remaining bare PR/issue references (#1234) to full GitHub links
|
||||
text = re.sub(r"(?<!\[)#(\d+)(?!\])", r"[#\1](https://github.com/XRPLF/rippled/pull/\1)", text)
|
||||
# Collapse multiple blank lines into one
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def extract_pr_number(commit_message):
|
||||
"""Extract PR number from commit message like 'Title (#1234)'."""
|
||||
match = re.search(r"#(\d+)", commit_message)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def should_skip(title):
|
||||
"""Check if a commit should be skipped."""
|
||||
return any(pattern.search(title) for pattern in SKIP_PATTERNS)
|
||||
|
||||
|
||||
def is_amendment(files):
|
||||
"""Check if any file in the list is features.macro."""
|
||||
return any("features.macro" in f for f in files)
|
||||
|
||||
|
||||
# --- Formatting ---
|
||||
|
||||
def format_commit_entry(sha, title, body="", files=None):
|
||||
"""Format an entry linked to a commit (no PR/Issue found)."""
|
||||
short_sha = sha[:7]
|
||||
url = f"https://github.com/XRPLF/rippled/commit/{sha}"
|
||||
parts = [
|
||||
f"- **{title.strip()}**",
|
||||
f" - Link: [{short_sha}]({url})",
|
||||
]
|
||||
if files:
|
||||
parts.append(f" - Files: {', '.join(files)}")
|
||||
if body:
|
||||
desc = re.sub(r"\s+", " ", clean_pr_body(body)).strip()
|
||||
if desc:
|
||||
parts.append(f" - Description: {desc}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def format_uncategorized_entry(pr_number, title, labels, body, files=None, link_type="pull"):
|
||||
"""Format an uncategorized entry with full context for AI sorting."""
|
||||
url = f"https://github.com/XRPLF/rippled/{link_type}/{pr_number}"
|
||||
parts = [
|
||||
f"- **{title.strip()}**",
|
||||
f" - Link: [#{pr_number}]({url})",
|
||||
]
|
||||
if labels:
|
||||
parts.append(f" - Labels: {', '.join(labels)}")
|
||||
if files:
|
||||
parts.append(f" - Files: {', '.join(files)}")
|
||||
if body:
|
||||
# Collapse to single line to prevent markdown formatting conflicts
|
||||
desc = re.sub(r"\s+", " ", body).strip()
|
||||
if desc:
|
||||
parts.append(f" - Description: {desc}")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def generate_markdown(version, release_date, amendment_diff, amendment_unchanged, amendment_entries, entries, authors, version_commit):
|
||||
"""Generate the full markdown release notes."""
|
||||
year = release_date.split("-")[0]
|
||||
parts = []
|
||||
|
||||
parts.append(f"""---
|
||||
category: {year}
|
||||
date: "{release_date}"
|
||||
template: '../../@theme/templates/blogpost'
|
||||
seo:
|
||||
title: Introducing XRP Ledger version {version}
|
||||
description: rippled version {version} is now available.
|
||||
labels:
|
||||
- rippled Release Notes
|
||||
markdown:
|
||||
editPage:
|
||||
hide: true
|
||||
---
|
||||
# Introducing XRP Ledger version {version}
|
||||
|
||||
Version {version} of `rippled`, the reference server implementation of the XRP Ledger protocol, is now available.
|
||||
|
||||
|
||||
## Action Required
|
||||
|
||||
If you run an XRP Ledger server, upgrade to version {version} as soon as possible to ensure service continuity.
|
||||
|
||||
|
||||
## Install / Upgrade
|
||||
|
||||
On supported platforms, see the [instructions on installing or updating `rippled`](../../docs/infrastructure/installation/index.md).
|
||||
|
||||
| Package | SHA-256 |
|
||||
|:--------|:--------|
|
||||
| [RPM for Red Hat / CentOS (x86-64)](https://repos.ripple.com/repos/rippled-rpm/stable/rippled-{version}-1.el9.x86_64.rpm) | `TODO` |
|
||||
| [DEB for Ubuntu / Debian (x86-64)](https://repos.ripple.com/repos/rippled-deb/pool/stable/rippled_{version}-1_amd64.deb) | `TODO` |
|
||||
|
||||
For other platforms, please [build from source](https://github.com/XRPLF/rippled/blob/master/BUILD.md). The most recent commit in the git log should be the change setting the version:
|
||||
|
||||
```text
|
||||
{version_commit}
|
||||
```
|
||||
|
||||
|
||||
## Full Changelog
|
||||
""")
|
||||
|
||||
# Amendments section (auto-sorted by features.macro detection with diff guidance for AI)
|
||||
parts.append("\n### Amendments\n")
|
||||
if amendment_diff or amendment_unchanged:
|
||||
included = sorted(name for name, include in amendment_diff.items() if include)
|
||||
excluded = sorted(name for name, include in amendment_diff.items() if not include)
|
||||
comment_lines = ["<!-- Amendment sorting instructions. Remove this comment after sorting."]
|
||||
if included:
|
||||
comment_lines.append(f"Include: {', '.join(included)}")
|
||||
if excluded:
|
||||
comment_lines.append(f"Exclude: {', '.join(excluded)}")
|
||||
if amendment_unchanged:
|
||||
comment_lines.append(f"Other amendments not part of this release: {', '.join(amendment_unchanged)}")
|
||||
comment_lines.append("-->")
|
||||
parts.append("\n".join(comment_lines) + "\n")
|
||||
for entry in amendment_entries:
|
||||
parts.append(entry)
|
||||
|
||||
# Remaining empty subsection headings for manual/AI sorting
|
||||
sections = [
|
||||
"Features", "Breaking Changes", "Bug Fixes",
|
||||
"Refactors", "Documentation", "Testing", "CI/Build",
|
||||
]
|
||||
for section in sections:
|
||||
parts.append(f"\n### {section}\n")
|
||||
|
||||
# Credits
|
||||
parts.append("\n\n## Credits\n")
|
||||
if authors:
|
||||
parts.append("The following RippleX teams and GitHub users contributed to this release:\n")
|
||||
else:
|
||||
parts.append("The following RippleX teams contributed to this release:\n")
|
||||
parts.append("- RippleX Engineering")
|
||||
parts.append("- RippleX Docs")
|
||||
parts.append("- RippleX Product")
|
||||
for author in sorted(authors):
|
||||
parts.append(f"- {author}")
|
||||
|
||||
parts.append("""
|
||||
|
||||
## Bug Bounties and Responsible Disclosures
|
||||
|
||||
We welcome reviews of the `rippled` code and urge researchers to responsibly disclose any issues they may find.
|
||||
|
||||
For more information, see:
|
||||
|
||||
- [Ripple's Bug Bounty Program](https://ripple.com/legal/bug-bounty/)
|
||||
- [`rippled` Security Policy](https://github.com/XRPLF/rippled/blob/develop/SECURITY.md)
|
||||
""")
|
||||
|
||||
# Unsorted entries with full context (after all published sections)
|
||||
parts.append("<!-- Sort the entries below into the Full Changelog subsections. Remove this comment after sorting. -->\n")
|
||||
for entry in entries:
|
||||
parts.append(entry)
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate rippled release notes")
|
||||
parser.add_argument("--from", dest="from_ref", required=True, help="Base ref (tag or branch)")
|
||||
parser.add_argument("--to", dest="to_ref", required=True, help="Target ref (tag or branch)")
|
||||
parser.add_argument("--date", help="Release date (YYYY-MM-DD). Defaults to today.")
|
||||
parser.add_argument("--output", help="Output file path (default: blog/<year>/rippled-<version>.md)")
|
||||
args = parser.parse_args()
|
||||
|
||||
args.date = args.date or date.today().isoformat()
|
||||
try:
|
||||
date.fromisoformat(args.date)
|
||||
except ValueError:
|
||||
print(f"Error: Invalid date format '{args.date}'. Use YYYY-MM-DD.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Fetching version info from {args.to_ref}...")
|
||||
version, version_commit = fetch_version_info(args.to_ref)
|
||||
print(f"Version: {version}")
|
||||
|
||||
year = args.date.split("-")[0]
|
||||
output_path = args.output or f"blog/{year}/rippled-{version}.md"
|
||||
|
||||
print(f"Fetching commits: {args.from_ref}...{args.to_ref}")
|
||||
commits = fetch_commits(args.from_ref, args.to_ref)
|
||||
print(f"Found {len(commits)} commits")
|
||||
|
||||
# Extract unique PR (in rare cases Issues) numbers and track authors
|
||||
pr_numbers = {}
|
||||
pr_shas = {} # PR/issue number → commit SHA (for file lookups on Issues)
|
||||
pr_bodies = {} # PR/issue number → commit body (for fallback descriptions)
|
||||
orphan_commits = [] # Commits with no PR/Issues link
|
||||
authors = set()
|
||||
|
||||
for commit in commits:
|
||||
full_message = commit["commit"]["message"]
|
||||
message = full_message.split("\n")[0]
|
||||
body = "\n".join(full_message.split("\n")[1:]).strip()
|
||||
sha = commit["sha"]
|
||||
author = commit["commit"]["author"]["name"]
|
||||
email = commit["commit"]["author"].get("email", "")
|
||||
|
||||
# Skip Ripple employees from credits
|
||||
login = (commit.get("author") or {}).get("login")
|
||||
if not email.lower().endswith("@ripple.com") and email not in EXCLUDED_EMAILS:
|
||||
if login:
|
||||
authors.add(f"@{login}")
|
||||
else:
|
||||
authors.add(author)
|
||||
|
||||
if should_skip(message):
|
||||
continue
|
||||
|
||||
pr_number = extract_pr_number(message)
|
||||
if pr_number:
|
||||
pr_numbers[pr_number] = message
|
||||
pr_shas[pr_number] = sha
|
||||
pr_bodies[pr_number] = body
|
||||
else:
|
||||
orphan_commits.append({"sha": sha, "message": message, "body": body})
|
||||
|
||||
print(f"Unique PRs after filtering: {len(pr_numbers)}")
|
||||
if orphan_commits:
|
||||
print(f"Commits without PR or Issue linked: {len(orphan_commits)}")
|
||||
# Fetch amendment diff between refs
|
||||
print(f"Comparing features.macro between {args.from_ref} and {args.to_ref}...")
|
||||
amendment_diff, amendment_unchanged = fetch_amendment_diff(args.from_ref, args.to_ref)
|
||||
if amendment_diff:
|
||||
for name, include in sorted(amendment_diff.items()):
|
||||
status = "include" if include else "exclude"
|
||||
print(f" Amendment {name}: {status}")
|
||||
else:
|
||||
print(" No amendment changes detected")
|
||||
|
||||
print(f"Building changelog entries...")
|
||||
|
||||
# Fetch all PR details in batches via GraphQL
|
||||
pr_details = fetch_prs_graphql(list(pr_numbers.keys()))
|
||||
|
||||
# Build entries, sorting amendments automatically
|
||||
amendment_entries = []
|
||||
entries = []
|
||||
for pr_number, commit_msg in pr_numbers.items():
|
||||
pr_data = pr_details.get(pr_number)
|
||||
|
||||
if pr_data:
|
||||
title = pr_data["title"]
|
||||
body = pr_data.get("body", "")
|
||||
labels = pr_data.get("labels", [])
|
||||
files = pr_data.get("files", [])
|
||||
link_type = pr_data.get("type", "pull")
|
||||
|
||||
# For issues (no files from GraphQL), fetch files from the commit
|
||||
if not files and pr_number in pr_shas:
|
||||
print(f" Building entry for Issue #{pr_number} via commit...")
|
||||
files = fetch_commit_files(pr_shas[pr_number])
|
||||
|
||||
if is_amendment(files) and amendment_diff:
|
||||
# Amendment entry — add to amendments section (AI will sort further)
|
||||
entry = format_uncategorized_entry(pr_number, title, labels, body, link_type=link_type)
|
||||
amendment_entries.append(entry)
|
||||
else:
|
||||
entry = format_uncategorized_entry(pr_number, title, labels, body, files, link_type)
|
||||
entries.append(entry)
|
||||
else:
|
||||
# Fallback to commit lookup for invalid PR and Issues link
|
||||
sha = pr_shas[pr_number]
|
||||
print(f" #{pr_number} not found as PR or Issue, building from commit {sha[:7]}...")
|
||||
files = fetch_commit_files(sha)
|
||||
if is_amendment(files) and amendment_diff:
|
||||
entry = format_commit_entry(sha, commit_msg, pr_bodies[pr_number])
|
||||
amendment_entries.append(entry)
|
||||
else:
|
||||
entry = format_commit_entry(sha, commit_msg, pr_bodies[pr_number], files)
|
||||
entries.append(entry)
|
||||
|
||||
# Build entries for orphan commits (no PR/Issue linked)
|
||||
for orphan in orphan_commits:
|
||||
sha = orphan["sha"]
|
||||
print(f" Building commit-only entry for {sha[:7]}...")
|
||||
files = fetch_commit_files(sha)
|
||||
if is_amendment(files) and amendment_diff:
|
||||
entry = format_commit_entry(sha, orphan["message"], orphan["body"])
|
||||
amendment_entries.append(entry)
|
||||
else:
|
||||
entry = format_commit_entry(sha, orphan["message"], orphan["body"], files)
|
||||
entries.append(entry)
|
||||
|
||||
# Generate markdown
|
||||
markdown = generate_markdown(version, args.date, amendment_diff, amendment_unchanged, amendment_entries, entries, authors, version_commit)
|
||||
|
||||
# Write output
|
||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
f.write(markdown)
|
||||
|
||||
print(f"\nRelease notes written to: {output_path}")
|
||||
|
||||
# Update blog/sidebars.yaml
|
||||
sidebars_path = "blog/sidebars.yaml"
|
||||
# Derive sidebar path and year from actual output path
|
||||
relative_path = output_path.removeprefix("blog/")
|
||||
sidebar_year = relative_path.split("/")[0]
|
||||
new_entry = f" - page: {relative_path}"
|
||||
try:
|
||||
with open(sidebars_path, "r") as f:
|
||||
sidebar_content = f.read()
|
||||
|
||||
if relative_path in sidebar_content:
|
||||
print(f"{sidebars_path} already contains {relative_path}")
|
||||
else:
|
||||
# Find the year group and insert at the top of its items
|
||||
year_marker = f" - group: '{sidebar_year}'"
|
||||
if year_marker not in sidebar_content:
|
||||
# Year group doesn't exist — find the right chronological position
|
||||
new_group = f" - group: '{sidebar_year}'\n expanded: false\n items:\n{new_entry}\n"
|
||||
# Find all existing year groups and insert before the first one with a smaller year
|
||||
year_groups = list(re.finditer(r" - group: '(\d{4})'", sidebar_content))
|
||||
insert_pos = None
|
||||
for match in year_groups:
|
||||
existing_year = match.group(1)
|
||||
if int(sidebar_year) > int(existing_year):
|
||||
insert_pos = match.start()
|
||||
break
|
||||
if insert_pos is not None:
|
||||
sidebar_content = sidebar_content[:insert_pos] + new_group + sidebar_content[insert_pos:]
|
||||
else:
|
||||
# New year is older than all existing — append at the end
|
||||
sidebar_content = sidebar_content.rstrip() + "\n" + new_group
|
||||
else:
|
||||
# Insert after the year group's "items:" line
|
||||
year_idx = sidebar_content.index(year_marker)
|
||||
items_idx = sidebar_content.index(" items:", year_idx)
|
||||
insert_pos = items_idx + len(" items:\n")
|
||||
sidebar_content = sidebar_content[:insert_pos] + new_entry + "\n" + sidebar_content[insert_pos:]
|
||||
|
||||
with open(sidebars_path, "w") as f:
|
||||
f.write(sidebar_content)
|
||||
print(f"Added {relative_path} to {sidebars_path}")
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: {sidebars_path} not found, skipping sidebar update", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user