This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch add-site-update-skill in repository https://gitbox.apache.org/repos/asf/tika.git
commit bfc1beaa0f8480cf358ed1eada9c4e72cbf25c2a Author: tallison <[email protected]> AuthorDate: Tue Jul 21 16:07:53 2026 -0400 Add skill for updating the site --- .skills/update-site-for-release/SKILL.md | 329 +++++++++++++++++++++ .skills/update-site-for-release/scripts/README.md | 37 +++ .../scripts/extract-tika-contribs.py | 248 ++++++++++++++++ .../scripts/extract-tika-issues.py | 149 ++++++++++ .../scripts/scaffold-stable-version.sh | 65 ++++ 5 files changed, 828 insertions(+) diff --git a/.skills/update-site-for-release/SKILL.md b/.skills/update-site-for-release/SKILL.md new file mode 100644 index 0000000000..a11dfeb155 --- /dev/null +++ b/.skills/update-site-for-release/SKILL.md @@ -0,0 +1,329 @@ +--- +name: update-site-for-release +description: > + Update/publish the Apache Tika website (tika-site SVN repo) for a release — + step 17 of the Release Process. Handles STABLE (full per-version docs + + javadoc) vs PREVIEW/beta (Changes page + Antora docs). Use for "update the + site", "publish the site for X.Y.Z", "the website part of the release". +--- + +# Update the Tika website for a release + +Step 17 ("Update Tika site") of the Release Process +(<https://cwiki.apache.org/confluence/spaces/TIKA/pages/109454070/Release+Process>). +Assumes the release (tag, artifacts, VOTE, dist promotion) is done; covers the +**website** only. + +Set these first: +- **`$SITE`** — `tika-site` **SVN** checkout (not git): `src/site/` (sources) + + `publish/` (generated, SVN-tracked, served; `mvn install` regenerates it and + auto-runs `svn add --force publish` via antrun). +- **`$SCRATCH`** — release working dir: unzipped src release, `CHANGES-<NEW>.txt`, + built javadoc. + +Scripts: `./scripts/`. Local paths + toolchain (Maven binary, JDKs) live in a +private companion skill under `~/.claude/skills/`. + +> **[HUMAN] gates — never do these yourself:** final `svn commit` (outward-facing, +> irreversible), JIRA "release", s.apache.org shortlink, announce emails. Prepare +> everything, show `svn status`, hand off. + +--- + +## 0. Inputs + release type + +| Input | Example | Notes | +|---|---|---| +| `NEW_VERSION` | `3.3.2` | the release | +| `RELEASE_TYPE` | `stable` | `stable` or `preview` (alpha/beta/BETA) | +| `PREV_STABLE` / `PREV_PREVIEW` | `3.3.1` / `4.0.0-beta-1` | current `tika.stable.version` / `tika.preview.version` | +| `PREV_TAG` / `NEW_TAG` | `3.3.1` / `3.3.2` | git tags, for the GitHub contributor query | +| `JIRA fixVersion` | `3.3.2` | **may differ** from the label (betas often use the base version, e.g. `4.0.0`) | +| `CHANGES` file | `$SCRATCH/CHANGES-3.3.2.txt` | notable-changes source | +| src release zip | `tika-<NEW>-src.zip` | **stable only** (javadoc) | +| release date | `2026-07-21` | doap.rdf + news blurb | + +Confirm current values in `src/site/pom.xml`. + +| Step | STABLE | PREVIEW / beta | +|---|---|---| +| `pom.xml` `<parent><version>` | → `<NEW>` | leave at stable | +| `pom.xml` `tika.stable.version` | → `<NEW>` | leave | +| `pom.xml` `tika.preview.version` | leave | → `<NEW>` | +| `src/site/apt/<NEW>/` | full 8-file set (scaffold from prev) | **only `index.apt`** | +| `site.xml` entry | full sub-menu, expanded | minimal `Changes` item under `docs/<major>-SNAPSHOT` | +| formats.apt | regenerate from `tika-app` jar | n/a (Antora docs) | +| javadoc | `clean install -Pfast` + `javadoc:aggregate` → `publish/<NEW>/api` (step 7) | n/a | +| Antora docs | n/a | `mvn package -pl docs` + `docs/publish-docs.sh` | +| Download page | automatic | automatic | + +doap.rdf, index.apt.vm news, verify, publish are common to both. + +--- + +## 1. `src/site/pom.xml` versions [AGENT] + +- **STABLE:** bump `<parent><version>` **and** `<tika.stable.version>` to `<NEW>`. + (parent version drives `${project.parent.version}` home-page links → must be + newest stable.) +- **PREVIEW:** bump only `<tika.preview.version>`; leave parent + stable. + +Download page auto-reads these — no manual edit. + +--- + +## 2. `src/site/site.xml` menu [AGENT] + +Current stable + current preview expanded; older = `collapse="true"`. + +- **STABLE:** new expanded block above the previous stable; add `collapse="true"` + to the old stable block: + ```xml + <item name="Apache Tika 3.3.2" href="3.3.2/index.html"> + <item name="Getting Started" href="3.3.2/gettingstarted.html"/> + <item name="Supported Formats" href="3.3.2/formats.html"/> + <item name="Parser API" href="3.3.2/parser.html"/> + <item name="Parser 5min Quick Start Guide" href="3.3.2/parser_guide.html"/> + <item name="Content and Language Detection" href="3.3.2/detection.html"/> + <item name="Configuring Tika" href="3.3.2/configuring.html"/> + <item name="Usage Examples" href="3.3.2/examples.html"/> + <item name="API Documentation" href="3.3.2/api/"/> + </item> + ``` +- **PREVIEW:** minimal block above the old preview (collapse it); `href` → Antora + snapshot, not an apt page: + ```xml + <item name="Apache Tika 4.0.0-beta-2" href="docs/4.0.0-SNAPSHOT"> + <item name="Changes" href="4.0.0-beta-2/index.html"/> + </item> + ``` + +--- + +## 3. Per-version apt docs `src/site/apt/<NEW>/` [AGENT] + +**STABLE** — scaffold (these docs are version-string-identical across 3.x): +```bash +./scripts/scaffold-stable-version.sh $SITE 3.3.1 3.3.2 +``` +Copies+bumps `configuring/detection/examples/parser/parser_guide/gettingstarted.apt`. Then: +- **formats.apt** — two parts: a hand-written top (license, intro, `%{toc}`, ~25 + prose format-family sections) down to the header line + `Full list of Supported Formats in "standard" artifacts`, then a generated flat + list below it. `--list-parser-details-apt` regenerates only the flat part. Copy + prev `formats.apt`, bump versions, replace everything **below** that header: + ```bash + java -jar <path>/tika-app-<NEW>.jar --list-parser-details-apt + ``` + **Do NOT truncate at the `%{toc}` line** — keep the prose sections. (Scaffold + skips `formats.apt` for this reason.) +- **index.apt** — step 4. + +**PREVIEW** — create only `index.apt` (copy prev preview's; title `Apache Tika +<NEW>`; fill step 4). No other files. + +--- + +## 4. Per-version `index.apt`: notable changes + contributors [AGENT + HUMAN] + +Shape (see `src/site/apt/3.3.1/index.apt`): license+title; "most notable changes…" +bullets; "The following people have contributed…" bullets; "See +{{https://s.apache.org/XXXX}} …". + +**Notable changes** — review output, keep only notable items: +```bash +./scripts/extract-tika-issues.py CHANGES-3.3.2.txt out-3.3.2.apt 3.3.2 +``` +Mirrors CHANGES verbatim; TIKA-####/Github-#### auto-linked; ALL-CAPS headers → +apt sections. + +**Contributors** — candidate list, RM curates: +```bash +./scripts/extract-tika-contribs.py 3.3.2 --prev-tag 3.3.1 --tag 3.3.2 > contribs.txt +# beta (fixVersion differs from label): +# ./scripts/extract-tika-contribs.py 4.0.0 --prev-tag 3.3.1 --tag 4.0.0-beta-1 +``` +Merges JIRA (reporters/assignees/comment authors) + GitHub commit/PR authors +(uses `gh` auth; resolves logins→names; case-insensitive sort; filters bots/AI). +Over-reports drive-by commenters, misses GitHub-issue-only commenters. **[HUMAN]** +prune / normalise / add. + +**Shortlink [HUMAN]** — `s.apache.org/XXXX` → the JIRA "issues fixed in <NEW>" +query; needs s.apache.org login. Ask the RM. + +--- + +## 5. `src/site/resources/doap.rdf` [AGENT] + +New `<release>` at the top. Ordering is by **date, not version** (a stable point +release can sit above an older-dated preview — 3.3.2/Jul-16 above 4.0.0-beta-1/Jul-3): +```xml + <release> + <Version> + <name>Apache Tika 3.3.2</name> + <created>2026-07-21</created> + <revision>3.3.2</revision> + </Version> + </release> +``` + +--- + +## 6. Home page `src/site/apt/index.apt.vm` [AGENT + HUMAN] + +1. New **Latest News** block at the top; its CHANGES link uses + `dist.apache.org/repos/dist/release/...` (live mirror): + ``` + [21 July 2026: Apache Tika Release] + Apache Tika 3.3.2 has been released! <one or two sentence summary>. + Please see the {{{https://dist.apache.org/repos/dist/release/tika/3.3.2/CHANGES-3.3.2.txt}CHANGES.txt}} + file for the full list of changes in the release and have a look at the download page for more information + on how to obtain Apache Tika 3.3.2. + ``` +2. **Repoint the superseded release's CHANGES link** (its artifacts get `svn rm`'d + from the live mirror at release): `dist.apache.org/repos/dist/release/tika/<PREV>/…` + → `archive.apache.org/dist/tika/<PREV>/…`. **[HUMAN]** confirm which version was + removed. + +--- + +## 7. Docs / Javadoc + +**STABLE — javadoc [AGENT].** NOT the wiki's `javadoc:aggregate-no-fork` (runs +against `tika-parent`, its relative `<sourcepath>` fails → `No source files for +package org.apache.tika`; wrong goal, not a JDK issue). From the unzipped src +release (its `./mvnw` is broken — use system `mvn`): +```bash +unzip tika-3.3.2-src.zip && cd tika-3.3.2 +mvn clean install -Pfast # ~4 min; module artifacts + full dep classpath +mvn javadoc:aggregate # FORKING goal (NOT -no-fork) +mkdir -p $SITE/publish/3.3.2 +mv target/reports/apidocs $SITE/publish/3.3.2/api +``` +Both steps matter: without `install` javadoc dies on `package org.slf4j does not +exist`; the forking `aggregate` (@aggregator) runs once on the root, `-no-fork` +breaks per-`pom`-module. Any modern JDK (11 and 25 verified). (`tika-server` +miredot docs discontinued — skip.) + +**PREVIEW (4.x) — Antora docs [AGENT]:** +```bash +cd tika-<NEW> # unzipped src release +./mvnw package -pl docs +./docs/publish-docs.sh $SITE/publish +``` +`publish-docs.sh` copies target/site into `publish/docs/`, flattens URLs, rewrites +the search index (has its own guards). + +--- + +## 8. Build + verify [AGENT] + +`tika-site` has no `./mvnw` → system `mvn`. **Build with Java 11** — it pins +maven-site-plugin 3.4 (2014), unreliable on newer JDKs; a Doxia error here means +wrong JDK, not a content problem (separate from step 7's JDK-agnostic javadoc). +```bash +cd "$SITE" +mvn clean install +``` +> **ALWAYS `clean install`, never bare `install`** — an incremental build leaves +> `publish/css/` stale → pages render with no CSS/sidebar. Fix is a `clean` +> rebuild, not a CSS edit. + +Build auto-copies target/site → `publish/`, strips timestamps, `svn add --force +publish`. Check: new version in the menu; news + download versions right; **pages +styled (CSS + sidebar)**; per-version pages + javadoc/Antora resolve. Preview: +`mvn site:run` → <http://localhost:8080>. + +--- + +## 9. Stage + hand off the commit [HUMAN] + +```bash +cd "$SITE" +svn status +svn add src/site/apt/<NEW> # + any other new files +# hand to the RM — do NOT run yourself: +# svn commit -m "Update website for <NEW> release." +``` + +**Big-commit caveat (stable):** `publish/<NEW>/api` is ~3,000 files / ~55 MB; a +single commit often **times out / `E000104 Connection reset by peer`** — this is +size, NOT auth (bad password = `Authentication failed`/403, and cached creds won't +re-prompt). Fixes: +- `http-timeout = 1800` in `~/.subversion/servers` `[global]`. +- Else commit the api in chunks, then the rest: + ```bash + svn commit --depth=empty publish/<NEW> publish/<NEW>/api \ + publish/<NEW>/api/org publish/<NEW>/api/org/apache \ + publish/<NEW>/api/org/apache/tika -m "<NEW> site: api dir skeleton" + for d in publish/<NEW>/api/org/apache/tika/*/; do + svn commit "$d" -m "<NEW> javadoc: $(basename "$d")" || break # parser/ ~1,300 files + done + svn commit publish/<NEW>/api -m "<NEW> javadoc: remaining api files" + svn commit -m "Update website for <NEW> release." + ``` +- Atomic per invocation → a failed commit rolls back; retry. Locked (`E155004`) → + `svn cleanup`. + +--- + +## 10. Confirm published + re-kick [HUMAN] + +svnwcsub maps `/www/tika.apache.org ← %(ASF)s/tika/site/publish`: **only a commit +touching `publish/` triggers a republish**, and it publishes the whole tree at +HEAD. Verify (cache-buster hits the origin, not Varnish): +```bash +curl -s -o /dev/null -w "%{http_code}\n" "https://tika.apache.org/<NEW>/index.html?cb=$(date +%s)" +``` +Want `200`. Still `404`/old minutes later → the web-node `svn up` choked on the +big commit. **Re-kick** with a trivial whitespace commit to a file **under +`publish/`** (e.g. a blank line in `publish/index.html`): +```bash +svn commit publish/index.html -m "Nudge svnwcsub to republish." +``` +Re-fires svnwcsub → `svn up` to HEAD (harmless; next build regenerates it). A +commit outside `publish/` won't trigger. Still stuck ~30 min → ping `#asfinfra`. + +--- + +## 11. Post-site [HUMAN] (context) + +- JIRA: "release" `<NEW>`; move stragglers to the next version. +- Announce to `user@`, `dev@`, `[email protected]`. +- Log at <https://reporter.apache.org/addrelease.html?tika>. +- CVEs fixed → update `security.apt` / per-version security notes, republish. + +--- + +## Checklist + +- [ ] stable vs preview decided +- [ ] `pom.xml` versions (parent+stable for stable; preview only for preview) +- [ ] `site.xml`: new entry added, previous same-track entry collapsed +- [ ] per-version apt docs (full set for stable / `index.apt` only for preview) +- [ ] `formats.apt` regenerated (stable) +- [ ] `index.apt`: notable changes + curated contributors + shortlink +- [ ] `doap.rdf` entry +- [ ] `index.apt.vm`: news block + superseded CHANGES link → archive +- [ ] javadoc → `publish/<NEW>/api` (stable) / Antora docs (preview) +- [ ] `mvn clean install` (never bare `install`); pages styled (CSS + sidebar) +- [ ] `svn status`/`svn add` done, commit handed to RM (chunk `api/` if it resets) +- [ ] live site 200: `https://tika.apache.org/<NEW>/index.html?cb=…` — else re-kick + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---|---|---| +| `aggregate-no-fork` → `No source files for package org.apache.tika` | runs against `tika-parent`; relative `<sourcepath>` can't resolve | use forking `javadoc:aggregate` after `clean install -Pfast` (step 7) | +| javadoc → `package org.slf4j does not exist` etc. | aggregate without a prior build → empty classpath | `mvn clean install -Pfast` first | +| pages unstyled (no CSS/sidebar) | incremental `install` left `publish/css/` stale | `mvn clean install` (never bare `install`) | +| site-plugin / Doxia error on `mvn install` | maven-site-plugin 3.4 on too-new a JDK | build with **Java 11** | +| notable-changes bullet split on a version number | old numeric heuristic (removed) | use the bundled script; re-run | +| contributors have bots/AI, or surname order | old behavior (fixed): filter + `str.casefold` sort | use bundled `extract-tika-contribs.py`; RM curates | +| commit `E175012 timed out` / `E000104 Connection reset` | ~55 MB api tree too big for one transaction | `http-timeout=1800`; chunk the `api/` (step 9). Size, not auth. | +| commit `Authentication failed` / 403 | genuinely bad/expired credential | `svn commit --username <you>` to re-cache | +| `svn: E155004 working copy locked` | prior commit died mid-transaction | `svn cleanup`, retry | +| committed, site still old even with `?cb=` (origin 404s/old) | web-node `svn up` choked on the big commit | re-kick: whitespace commit under `publish/` (step 10); stuck ~30 min → `#asfinfra` | +| home-page CHANGES link 404s for the previous release | it was `svn rm`'d from the live dist mirror | repoint to `archive.apache.org/dist/tika/<prev>/CHANGES-<prev>.txt` (step 6) | diff --git a/.skills/update-site-for-release/scripts/README.md b/.skills/update-site-for-release/scripts/README.md new file mode 100644 index 0000000000..e55ab0a387 --- /dev/null +++ b/.skills/update-site-for-release/scripts/README.md @@ -0,0 +1,37 @@ +# Helper scripts for `update-site-for-release` + +Python 3. No third-party deps (stdlib only). Run from anywhere. + +| Script | Purpose | Key gotcha | +|---|---|---| +| `extract-tika-issues.py` | `CHANGES-X.Y.Z.txt` → apt "notable changes" bullet list | Mirrors CHANGES verbatim (bullets + all-caps section headers); review to keep only notable items. Arg order is `CHANGES OUTPUT VERSION` (guarded against the common swap). | +| `extract-tika-contribs.py` | Candidate contributor list from **JIRA + GitHub** | **Candidate only** — RM must curate. `FIX_VERSION` may differ from the release label (betas). Pass `--prev-tag`/`--tag` to include GitHub PR authors. Uses `gh auth token` automatically. | +| `scaffold-stable-version.sh` | Copy previous stable's apt docs → new version, bump version string | STABLE only. Skips `formats.apt`/`index.apt` on purpose (regenerate those). | + +## Origin + +`extract-tika-issues.py` and the idea behind `extract-tika-contribs.py` come from +Chris Mattmann's [`chrismattmann/apachestuff`](https://github.com/chrismattmann/apachestuff) +repo. The original `extract-tika-contribs` was a bash one-liner that scraped the +JIRA "printable" search HTML (`grep ViewProfile | cut | sort | uniq`) behind a +`tika` shell alias — fragile, capped at 100 rows, and blind to GitHub-only +contributors. This version queries the JIRA REST API and the GitHub compare API +instead. (`apachestuff` also has `name_to_committer_id.py`; the Tika site process +lists contributors by display name and never uses committer ids, so it is +intentionally not bundled here.) + +## Examples + +```bash +# Notable changes for a stable release +./extract-tika-issues.py /path/to/CHANGES-3.3.2.txt out-3.3.2.apt 3.3.2 + +# Contributors, stable (JIRA fixVersion == label) +./extract-tika-contribs.py 3.3.2 --prev-tag 3.3.1 --tag 3.3.2 + +# Contributors, beta (JIRA fixVersion is the base version, tag is the label) +./extract-tika-contribs.py 4.0.0 --prev-tag 3.3.1 --tag 4.0.0-beta-1 + +# Scaffold the new stable version's apt docs +./scaffold-stable-version.sh /path/to/tika-site 3.3.1 3.3.2 +``` diff --git a/.skills/update-site-for-release/scripts/extract-tika-contribs.py b/.skills/update-site-for-release/scripts/extract-tika-contribs.py new file mode 100755 index 0000000000..d6d35d3466 --- /dev/null +++ b/.skills/update-site-for-release/scripts/extract-tika-contribs.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Emit a CANDIDATE Apache-Tika contributor list (apt bullet format) for a release, +merging two sources: + + 1. JIRA -- reporters, assignees, and comment authors on every issue in the + fix version. (Replaces Chris Mattmann's original `extract-tika-contribs` + bash script, chrismattmann/apachestuff, which scraped the JIRA printable + HTML via `grep ViewProfile | cut | sort | uniq` behind a `tika` shell alias.) + + 2. GitHub -- commit authors and Co-authored-by trailers between the previous + release tag and the new one. Increasingly contributors go straight to + GitHub and never touch JIRA, so this catches PR authors the JIRA query + alone misses. + + STILL A CANDIDATE, not the final list. It over-reports (drive-by commenters, + bots) and can under-report -- e.g. someone whose only contribution was a + comment on a GitHub *issue* (not a PR) appears in neither source. The tika-site + 3.3.1 index.apt lists "Chengxin Xu", who is absent from both JIRA fixVersion + 3.3.1 and the 3.3.0..3.3.1 git range. The release manager MUST review the + output, prune noise, normalise names, and add anyone missing. + +Usage: + extract-tika-contribs.py FIX_VERSION [--prev-tag T] [--tag T] + [--repo apache/tika] [--project TIKA] + [--no-github] [--include-bots] + + FIX_VERSION JIRA fixVersion. NOT always the release label -- for a + beta, issues are often tagged with the base version + (4.0.0-beta-1 contributors came from fixVersion "4.0.0"). + --prev-tag / --tag git tags bounding this release, e.g. --prev-tag 3.3.1 + --tag 3.3.2. If omitted, the GitHub half is skipped. + --no-github JIRA only. + +GitHub auth: uses GITHUB_TOKEN / GH_TOKEN if set, else `gh auth token`, else +anonymous (60 req/hr). `gh` is the easiest -- if you can run gh, this just works. + +Examples: + ./extract-tika-contribs.py 3.3.2 --prev-tag 3.3.1 --tag 3.3.2 + ./extract-tika-contribs.py 4.0.0 --prev-tag 3.3.1 --tag 4.0.0-beta-1 +""" +import argparse +import json +import re +import subprocess +import sys +import urllib.parse +import urllib.request + +JIRA = "https://issues.apache.org/jira" +GITHUB = "https://api.github.com" + +# Automated / non-human accounts (JIRA display names or GitHub author names). +# Matched case-insensitively as substrings. +BOT_DENYLIST = [ + "asf github bot", "asf subversion and git services", "hudson", "jenkins", + "gridgain integration", "jira bot", "githubbot", + "dependabot", "[bot]", "copilot", "github-actions", + # AI coding assistants that show up as commit/co-authors. Kept specific so a + # real person named e.g. "Claude Warren" is NOT filtered. + "claude sonnet", "claude opus", "claude haiku", "claude code", + "chatgpt", "openai", "gpt-4", "gpt-5", "cursor agent", "devin ai", +] + +# Co-author email domains that identify AI assistants regardless of display name. +AI_EMAIL_DOMAINS = ["anthropic.com", "openai.com"] + + +def is_bot(name): + low = name.lower() + return any(bot in low for bot in BOT_DENYLIST) + + +def _get_json(url, headers): + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=60) as resp: + return json.load(resp) + + +# ---------------------------------------------------------------- JIRA ---- +def collect_jira(fix_version, project): + jql = f'project = {project} AND fixVersion = "{fix_version}"' + names, start_at, total, issue_count = set(), 0, None, 0 + while total is None or start_at < total: + params = urllib.parse.urlencode({ + "jql": jql, "fields": "reporter,assignee,comment", + "startAt": start_at, "maxResults": 100, + }) + data = _get_json(f"{JIRA}/rest/api/2/search?{params}", {"Accept": "application/json"}) + total = data.get("total", 0) + issues = data.get("issues", []) + if not issues: + break + for issue in issues: + issue_count += 1 + f = issue.get("fields", {}) + for who in (f.get("reporter"), f.get("assignee")): + if who and who.get("displayName"): + names.add(who["displayName"].strip()) + for c in (f.get("comment") or {}).get("comments", []): + a = c.get("author") or {} + if a.get("displayName"): + names.add(a["displayName"].strip()) + start_at += len(issues) + return names, issue_count, total or 0 + + +# -------------------------------------------------------------- GitHub ---- +def _github_token(): + import os + tok = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if tok: + return tok.strip() + try: + out = subprocess.run(["gh", "auth", "token"], capture_output=True, text=True, timeout=15) + if out.returncode == 0 and out.stdout.strip(): + return out.stdout.strip() + except (FileNotFoundError, subprocess.SubprocessError): + pass + return None + + +def _resolve_login(login, headers, cache): + """GitHub login -> display name (falls back to the login). Collapses + login-vs-real-name duplicates like THausherr / Tilman Hausherr.""" + if login in cache: + return cache[login] + name = login + try: + data = _get_json(f"{GITHUB}/users/{login}", headers) + if data.get("name"): + name = data["name"].strip() + except Exception: # noqa: BLE001 - keep the login on any lookup failure + pass + cache[login] = name + return name + + +def collect_github(repo, prev_tag, tag): + headers = {"Accept": "application/vnd.github+json", "User-Agent": "update-site-for-release"} + tok = _github_token() + if tok: + headers["Authorization"] = f"Bearer {tok}" + names, logins, page, total_commits = set(), set(), 1, None + while True: + url = f"{GITHUB}/repos/{repo}/compare/{prev_tag}...{tag}?per_page=100&page={page}" + data = _get_json(url, headers) + if "commits" not in data: + raise RuntimeError(data.get("message", "unexpected GitHub response")) + total_commits = data.get("total_commits", total_commits) + commits = data["commits"] + if not commits: + break + for c in commits: + login = (c.get("author") or {}).get("login") + if login: + logins.add(login) + else: + # No linked GitHub account (rare) -- fall back to the git author name. + a = c["commit"]["author"] + if a.get("name"): + names.add(a["name"].strip()) + for m in re.finditer(r"Co-authored-by:\s*(.+?)\s*<([^>]*)>", c["commit"]["message"]): + nm, email = m.group(1).strip(), m.group(2).strip().lower() + if any(dom in email for dom in AI_EMAIL_DOMAINS): + continue + names.add(nm) + if len(commits) < 100: + break + page += 1 + # Resolve logins to display names so they dedupe against JIRA/co-author names. + cache = {} + for login in logins: + names.add(_resolve_login(login, headers, cache)) + return names, total_commits or 0, bool(tok) + + +# ---------------------------------------------------------------- main ---- +def main(): + ap = argparse.ArgumentParser(description="Candidate Tika contributor list (JIRA + GitHub).") + ap.add_argument("fix_version") + ap.add_argument("--prev-tag") + ap.add_argument("--tag") + ap.add_argument("--repo", default="apache/tika") + ap.add_argument("--project", default="TIKA") + ap.add_argument("--no-github", action="store_true") + ap.add_argument("--include-bots", action="store_true") + args = ap.parse_args() + + all_names = set() + notes = [] + + try: + jira_names, issue_count, total = collect_jira(args.fix_version, args.project) + all_names |= jira_names + notes.append(f"JIRA fixVersion={args.fix_version}: {len(jira_names)} name(s) " + f"from {issue_count} issue(s).") + except Exception as e: # noqa: BLE001 + notes.append(f"JIRA query FAILED: {e}") + + do_github = not args.no_github and args.prev_tag and args.tag + if do_github: + try: + gh_names, ncommits, authed = collect_github(args.repo, args.prev_tag, args.tag) + all_names |= gh_names + notes.append(f"GitHub {args.prev_tag}..{args.tag}: {len(gh_names)} " + f"name(s) from {ncommits} commit(s) " + f"[{'authenticated' if authed else 'anonymous'}].") + except Exception as e: # noqa: BLE001 + notes.append(f"GitHub query FAILED: {e}") + elif not args.no_github: + notes.append("GitHub half SKIPPED: pass --prev-tag and --tag to include PR authors.") + + if not args.include_bots: + all_names = {n for n in all_names if not is_bot(n)} + + # Simple case-insensitive alphabetical sort by the full display string + # (first-name-first, as written) -- the site's historical convention, which + # came from the old bash script piping through `sort`. NOT a last-name sort. + for name in sorted(all_names, key=str.casefold): + print(f" * {name}") + + print("", file=sys.stderr) + for n in notes: + print(f"# {n}", file=sys.stderr) + print(f"# {len(all_names)} candidate contributor(s) total. CANDIDATE LIST -- review " + f"before use: prune drive-by/bot names, normalise duplicates (login vs real " + f"name), and add GitHub-issue commenters not captured by either source.", + file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.skills/update-site-for-release/scripts/extract-tika-issues.py b/.skills/update-site-for-release/scripts/extract-tika-issues.py new file mode 100755 index 0000000000..c4b73c6ca6 --- /dev/null +++ b/.skills/update-site-for-release/scripts/extract-tika-issues.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Turn a CHANGES-X.Y.Z.txt release section into the apt "notable changes" list. + +Python 3 port of Chris Mattmann's extract-tika-issues.py (chrismattmann/apachestuff), +extended to recognise " BREAKING CHANGES" / " NEW FEATURES" style section headers +and to join wrapped continuation lines. + +Usage: + extract-tika-issues.py CHANGES_FILE OUTPUT_FILE RELEASE_VERSION + + CHANGES_FILE path to CHANGES-X.Y.Z.txt (input) + OUTPUT_FILE path to write the apt fragment (output; will be overwritten) + RELEASE_VERSION e.g. 3.3.2 -- must match a "Release X.Y.Z - MM/dd/yyyy" header + +Example: + ./extract-tika-issues.py CHANGES-3.3.2.txt out-3.3.2.apt 3.3.2 + +NOTE: the output is a STARTING POINT, not final copy. The numeric heuristic +mis-splits sentences that contain version numbers (e.g. "Jetty 11 -> 12.0.36" +becomes a bogus bullet). Hand-edit the result before pasting it into index.apt, +and keep only the genuinely notable items -- this is not meant to be the whole +changelog. +""" +import re +import sys + +USAGE = ("Usage: extract-tika-issues.py CHANGES_FILE OUTPUT_FILE RELEASE_VERSION\n" + " e.g. extract-tika-issues.py CHANGES-3.3.2.txt out-3.3.2.apt 3.3.2") + + +def looks_like_version(s): + return re.fullmatch(r"\d+\.\d+\.\d+(?:[-.].+)?", s.strip()) is not None + + +def main(): + if len(sys.argv) != 4: + print(USAGE, file=sys.stderr) + return 2 + + changesFile, outputFile, relVersion = sys.argv[1], sys.argv[2], sys.argv[3] + + # Guard the classic arg-order slip (OUTPUT and VERSION swapped), which + # silently writes a file literally named after the output path. + if looks_like_version(outputFile) and not looks_like_version(relVersion): + print(f"ERROR: argument 2 ('{outputFile}') looks like a version and argument 3 " + f"('{relVersion}') does not.\n" + f"Arguments are: CHANGES_FILE OUTPUT_FILE RELEASE_VERSION -- did you swap " + f"the last two?\n{USAGE}", file=sys.stderr) + return 2 + + versionChangeLog = {} + version = None + + # Tika CHANGES.txt grammar: + # "Release X.Y.Z - M/D/YYYY" version header + # " * text ..." a changelog entry (may wrap over indented lines) + # " BREAKING CHANGES" an all-caps section header (optional; some releases) + # blank line separates entries + release_re = re.compile(r".*\d{1,2}/\d{1,2}/\d{4}.*") + bullet_re = re.compile(r"^\s*\*\s+(.*)") + # All-caps line = section header. Restricting to A-Z and spaces (no digits, + # slashes, or lowercase) keeps wrapped prose from being mistaken for a header. + # NOTE: the old numeric-list heuristic was removed -- it mangled entries + # containing version numbers ("Port the 4.x SAX-based parsers..." -> "x SAX..."). + section_re = re.compile(r"^[A-Z][A-Z ]{2,38}$") + SECTION_MARKER = "__SECTION__:" + + with open(changesFile, mode="r", encoding="utf-8") as cf: + lines = cf.readlines() + + versionIssues = [] + issueTxt = "" + for line in lines: + stripped = line.strip() + + if release_re.match(line.rstrip()): + if issueTxt: + versionIssues.append(issueTxt) + issueTxt = "" + if version is not None: + versionChangeLog[version] = versionIssues + versionIssues = [] + version = line.rsplit(" - ", 1)[0].strip() + continue + if version is None or not stripped: # preamble or entry separator + continue + if "notable changes" in line or set(stripped) <= {"-"}: + continue + m = bullet_re.match(line) + if m: # new changelog entry + if issueTxt: + versionIssues.append(issueTxt) + issueTxt = m.group(1).strip() + elif section_re.match(stripped): # section header + if issueTxt: + versionIssues.append(issueTxt) + issueTxt = "" + versionIssues.append(SECTION_MARKER + stripped.title()) + else: # continuation of current entry + issueTxt = (issueTxt + " " + stripped) if issueTxt else stripped + + if issueTxt: + versionIssues.append(issueTxt) + if version is not None: + versionChangeLog[version] = versionIssues + + key = "Release " + relVersion + if key not in versionChangeLog: + print(f"ERROR: no '{key}' section found in {changesFile}.\n" + f"Sections present: {sorted(versionChangeLog)}", file=sys.stderr) + return 1 + + with open(outputFile, "w", encoding="utf-8") as of: + of.write("Apache Tika " + relVersion + "\n\n") + of.write("\t The most notable changes in Tika " + relVersion + + " over the previous release are:\n\n") + for issue in versionChangeLog[key]: + if issue.startswith(SECTION_MARKER): + of.write("* " + issue[len(SECTION_MARKER):] + "\n\n") + continue + out = re.sub(r"TIKA-(\d+)", + r"{{{http://issues.apache.org/jira/browse/TIKA-\1}TIKA-\1}}", issue) + out = re.sub(r"Github-(\d+)", + r"{{{http://github.com/apache/tika/pull/\1}Github-\1}}", out, flags=re.I) + of.write("\t * " + out + "\n\n") + + print(f"Wrote {outputFile} for release {relVersion} " + f"({len(versionChangeLog[key])} entries). Review & hand-edit before pasting.", + file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.skills/update-site-for-release/scripts/scaffold-stable-version.sh b/.skills/update-site-for-release/scripts/scaffold-stable-version.sh new file mode 100644 index 0000000000..fa8709f619 --- /dev/null +++ b/.skills/update-site-for-release/scripts/scaffold-stable-version.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Scaffold a new STABLE version's apt docs by copying the previous stable +# version's directory and bumping the version string. Safe because the +# per-version apt docs (configuring/detection/examples/parser/parser_guide/ +# gettingstarted) are identical across 3.x releases apart from the version +# string. formats.apt and index.apt are intentionally NOT copied: they must be +# regenerated (formats from `tika-app --list-parser-details-apt`, index from +# the extract-tika-* scripts + hand-editing). +# +# PREVIEW/beta releases do NOT use this -- they get only a hand-written +# index.apt. See SKILL.md. +# +# Usage: +# scaffold-stable-version.sh <tika-site-root> <prev-version> <new-version> +# Example: +# scaffold-stable-version.sh /path/to/tika-site 3.3.1 3.3.2 +set -euo pipefail + +SITE_ROOT="${1:?usage: scaffold-stable-version.sh <tika-site-root> <prev-version> <new-version>}" +PREV="${2:?missing <prev-version>}" +NEW="${3:?missing <new-version>}" + +APT="${SITE_ROOT}/src/site/apt" +SRC="${APT}/${PREV}" +DST="${APT}/${NEW}" + +[[ -d "${SRC}" ]] || { echo "Previous version dir not found: ${SRC}" >&2; exit 1; } +[[ -e "${DST}" ]] && { echo "Target already exists: ${DST} (refusing to overwrite)" >&2; exit 1; } + +# Files that are pure version-string bumps of the previous release. +COPY_BUMP=(configuring.apt detection.apt examples.apt parser.apt parser_guide.apt gettingstarted.apt) + +mkdir -p "${DST}" +for f in "${COPY_BUMP[@]}"; do + if [[ -f "${SRC}/${f}" ]]; then + sed "s/${PREV//./\\.}/${NEW}/g" "${SRC}/${f}" > "${DST}/${f}" + echo " bumped ${f}" + else + echo " WARN: ${SRC}/${f} missing, skipped" >&2 + fi +done + +echo "" +echo "Scaffolded ${DST} from ${PREV}." +echo "STILL TO DO (not done by this script):" +echo " * formats.apt -- regenerate: java -jar tika-app-${NEW}.jar --list-parser-details-apt" +echo " (prepend the same header block as ${SRC}/formats.apt, then the list)" +echo " * index.apt -- build from extract-tika-issues.py + extract-tika-contribs.py, hand-edit" +echo " * git/svn add -- 'svn add ${DST}' once the two files above exist" +echo " * Verify each bumped file's version references look right (diff against ${SRC})."
