From b9917505cd4acef4d6eef8692ff6e8be1a1683c6 Mon Sep 17 00:00:00 2001 From: MeepMr <67143880+MeepMr@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:02:41 +0200 Subject: [PATCH] Drop the issue-migration script: a one-off job is not repo content The migration from GitHub issues to Gitea runs exactly once, by hand over the API. Checking a script in for it would leave permanently maintained code behind for a task that stops existing after its single run - and worse, it would read to every later visitor as a tool meant to be reused. A one-off move is carried out once and is history afterwards; it is not a part of the repository. The one reference in CLAUDE.md is rewritten rather than deleted: that the issue numbers do not survive the move is still true and still worth knowing, so the bullet now says a `#n` in this repo means the GitHub issue, and that what to do about the old references is open in #225. --- CLAUDE.md | 2 +- ci/migrate-issues-to-gitea.sh | 256 ---------------------------------- 2 files changed, 1 insertion(+), 257 deletions(-) delete mode 100755 ci/migrate-issues-to-gitea.sh diff --git a/CLAUDE.md b/CLAUDE.md index 23aa80f..1a46d63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,7 +27,7 @@ The repository is moving from github.com to the Gitea instance JLab-Git (#225). - **Today the merge flow still runs on GitHub.** Issues, pull requests and merges happen there until the maintainer switches Actions off and turns on the mirror. Until then, **do not push to `JLab-Git/main`**: with publish workflows armed on both sides, two CIs publish into the same registry, and that registry accepts a re-upload of an existing version silently, replacing the artifact (#160). Pushing a *branch* to Gitea is fine — only `main` triggers a publish. - **`gh` only as `op plugin run -- gh`**, `tea` only as `op plugin run -- tea`. A bare `gh` falls back to the wrong identity. Every call costs a 1Password confirmation, so bundle them and never poll. - **After the move: no merge queue.** Gitea has none. Its place is taken by **auto-merge on green checks** — the PR is armed once and merges itself when the required checks pass. The manager still decides *whether* to arm it; rule 6 above is unchanged: the user merges, and an agent never merges its own PR. -- **Issue numbers will not survive the move.** Gitea renumbers, and issues and PRs share one counter. A `#n` written today keeps pointing at the GitHub issue; `ci/migrate-issues-to-gitea.sh` writes the old→new mapping but deliberately rewrites nothing. +- **Issue numbers will not survive the move.** Gitea renumbers, and issues and PRs share one counter, so a `#n` written today will not land on the same number over there. The migration itself is the maintainer's one-off job over the API; how the old references are dealt with afterwards is open and decided in #225. Until that is decided, a `#n` anywhere in this repo means the GitHub issue. - **Renovate is already running on the Gitea side, and not from this repo.** A shared `jlab-renovate-bot` instance serves the other repositories there and has this one enrolled; it reads our `renovate.json5` and its `postUpgradeTasks` hook is allow-listed (both verified on its first run). **Do not add a Renovate workflow here** — a second runner against the same repo means duplicate PRs and two schedulers rewriting each other's branches. README, "Renovate on Gitea", has the evidence. ## Keep the docs current diff --git a/ci/migrate-issues-to-gitea.sh b/ci/migrate-issues-to-gitea.sh deleted file mode 100755 index a5aed2f..0000000 --- a/ci/migrate-issues-to-gitea.sh +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env bash -# Copy this repository's GitHub issues into the Gitea repository, over the API. -# -# Part of the move to JLab-Git (#225). The Gitea repo already exists and keeps -# its history, so Gitea's own "migrate from GitHub" is not available - it -# insists on creating the repository itself. What is left is this: read the -# issues from GitHub, write them to Gitea. -# -# WHAT THIS SCRIPT CANNOT DO - read this before running it -# -# Three losses are inherent to the API path, not bugs to be fixed later. They -# are named here because each of them is invisible in the result: the migrated -# issue looks complete, and only the detail is gone. -# -# 1. COMMENT AUTHORSHIP. Every issue and every comment is created by the -# token's user. Gitea has no "create as another user" outside the admin -# sudo API, and the GitHub accounts do not exist on the instance anyway. -# The script therefore prepends an attribution line to each body naming -# the original author and date. A thread of three people still reads as -# three people - but the avatar, the @mention and every "authored by" -# filter say the token's user wrote all of it. -# -# 2. CROSS-REFERENCES BETWEEN ISSUES. Gitea hands out numbers sequentially, -# and issues and pull requests share one counter on both systems, so -# GitHub's #218 does not become Gitea's #218. Every "#" inside a body, -# a comment, a values.yaml, a NOTES.txt or a commit message then points at -# a different entry - silently, because a wrong number still renders as a -# valid link. This script does NOT rewrite them. It writes the mapping to -# --map-file (oldnew, one per line) so the rewrite can be decided and -# done separately; #225 lists the options and none of them is decided. -# -# 3. ATTACHMENTS. Images and files in a body live behind GitHub's -# user-content CDN. The markdown is copied verbatim, so the links keep -# working exactly as long as the GitHub repo stays reachable - and break -# the day it does not. Re-uploading them is a separate job (download, -# POST to Gitea's attachment API, rewrite the link) and deliberately not -# part of this one. -# -# Smaller ones, for completeness: milestones, assignees, reactions, review -# threads and the closed pull requests are not migrated. Pull requests are -# skipped entirely - a PR without its branch and diff is a dead entry, and -# every branch it referred to is merged or gone. -# -# USAGE -# -# GITEA_URL=https:// GITEA_TOKEN= \ -# ci/migrate-issues-to-gitea.sh --dry-run -# -# --state open|closed|all which issues (default: open) -# --limit N stop after N issues (a trial run on real data) -# --map-file PATH where old->new lands (default: issue-map.tsv) -# --source OWNER/REPO GitHub source (default: jk-cluster/helm-charts) -# --target OWNER/REPO Gitea target (default: jlab-cluster/helm-charts) -# -# The instance address comes from the environment, never from this file - the -# same rule the publish workflow follows for the registry URL. -# -# IDEMPOTENCE. The map file is also the guard: an issue already listed there is -# skipped. Delete the file and the next run creates everything a second time, -# so keep it next to the migration and do not regenerate it from scratch. -# -# REQUIREMENTS. `gh` authenticated against github.com (in this setup that means -# `op plugin run -- gh`, otherwise the identity falls back to the wrong user), -# plus `curl`, `jq` and a Gitea token with write access to issues on the target -# repo. - -set -euo pipefail - -DRY_RUN=0 -STATE=open -LIMIT=0 -MAP_FILE=issue-map.tsv -SOURCE_REPO=jk-cluster/helm-charts -TARGET_REPO=jlab-cluster/helm-charts - -while [ $# -gt 0 ]; do - case "$1" in - --dry-run) DRY_RUN=1 ;; - --state) STATE="${2:?--state needs open|closed|all}"; shift ;; - --limit) LIMIT="${2:?--limit needs a number}"; shift ;; - --map-file) MAP_FILE="${2:?--map-file needs a path}"; shift ;; - --source) SOURCE_REPO="${2:?--source needs OWNER/REPO}"; shift ;; - --target) TARGET_REPO="${2:?--target needs OWNER/REPO}"; shift ;; - -h | --help) sed -n '2,66p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; - *) echo "unknown argument: $1" >&2; exit 2 ;; - esac - shift -done - -case "$STATE" in - open | closed | all) ;; - *) echo "--state must be open, closed or all (got: $STATE)" >&2; exit 2 ;; -esac - -for tool in gh curl jq; do - command -v "$tool" >/dev/null 2>&1 || { - echo "missing required tool: $tool" >&2 - exit 1 - } -done - -# The target address is required even for a dry run: a dry run that skips the -# address proves less than it appears to, and the run would then differ from -# the real one in more than the writes. -: "${GITEA_URL:?set GITEA_URL to the instance base URL, e.g. https://git.example.com}" -GITEA_URL="${GITEA_URL%/}" -if [ "$DRY_RUN" -eq 0 ]; then - : "${GITEA_TOKEN:?set GITEA_TOKEN to a token with issue write access on $TARGET_REPO}" -fi - -API="$GITEA_URL/api/v1" - -# The token goes into a curl config file rather than onto the command line, so -# it never appears in the process list - the same reason publish-helm-chart.yml -# pipes its options into `curl --config -`. Here it is a file and not stdin, -# because stdin carries the JSON body. mktemp creates it 0600. -CURL_CFG="$(mktemp)" -trap 'rm -f "$CURL_CFG"' EXIT -if [ "$DRY_RUN" -eq 0 ]; then - printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" >"$CURL_CFG" -fi - -# --fail-with-body turns 401/403/422 into a non-zero exit and still prints the -# response. Without it curl exits 0 on all of them and the migration would -# report success having written nothing. -gitea() { - local method="$1" path="$2" - if [ "$method" = GET ]; then - curl --config "$CURL_CFG" --silent --show-error --fail-with-body \ - --request GET "$API/$path" - else - curl --config "$CURL_CFG" --silent --show-error --fail-with-body \ - --request "$method" \ - --header "Content-Type: application/json" \ - --data-binary @- "$API/$path" - fi -} - -say() { printf '%s\n' "$*"; } - -[ "$DRY_RUN" -eq 1 ] && say "DRY RUN - nothing is written to $GITEA_URL" -say "source: github.com/$SOURCE_REPO target: $GITEA_URL/$TARGET_REPO" -say "state: $STATE map file: $MAP_FILE" -say "" - -touch "$MAP_FILE" - -# --- labels ------------------------------------------------------------------ -# -# A label that does not exist on the target is dropped by Gitea's issue API -# without an error, so they are created on demand below. Names are compared -# case-sensitively, which is how both systems store them. -existing_labels="" -if [ "$DRY_RUN" -eq 0 ]; then - existing_labels="$(gitea GET "repos/$TARGET_REPO/labels?limit=100" | jq -r '.[].name')" -fi - -# --- issues ------------------------------------------------------------------ -# -# --paginate walks every page; PRs are filtered out here rather than by an API -# flag, because GitHub's issues endpoint returns them too and only the -# .pull_request key tells them apart. Oldest first, so the Gitea numbers come -# out in the same order as the originals even though the values differ. -issues_json="$(gh api --paginate \ - -H "Accept: application/vnd.github+json" \ - "repos/$SOURCE_REPO/issues?state=$STATE&per_page=100&sort=created&direction=asc" | - jq -s 'add | map(select(.pull_request == null))')" - -total="$(jq 'length' <<<"$issues_json")" -say "found $total issue(s) on GitHub (pull requests excluded)" -say "" - -count=0 -while read -r number; do - [ -n "$number" ] || continue - - if grep -qE "^${number} " "$MAP_FILE"; then - say "#$number already migrated (in $MAP_FILE) - skipped" - continue - fi - - if [ "$LIMIT" -gt 0 ] && [ "$count" -ge "$LIMIT" ]; then - say "--limit $LIMIT reached, stopping" - break - fi - count=$((count + 1)) - - issue="$(jq --argjson n "$number" '.[] | select(.number == $n)' <<<"$issues_json")" - title="$(jq -r '.title' <<<"$issue")" - state="$(jq -r '.state' <<<"$issue")" - labels="$(jq -c '[.labels[].name]' <<<"$issue")" - - # The provenance line is the only edit made to a body. It carries the old - # number, which is what makes an un-rewritten "#" elsewhere traceable at - # all, and it names the original author because the API cannot. - body="$(jq -r ' - "_Migriert von GitHub: " + .html_url + " - urspruenglich von @" + .user.login - + " am " + (.created_at | split("T")[0]) + "._\n\n" - + (.body // "")' <<<"$issue")" - - say "#$number $title [$state, labels: $labels]" - - if [ "$DRY_RUN" -eq 1 ]; then - comment_count="$(jq -r '.comments' <<<"$issue")" - suffix="" - [ "$state" = closed ] && suffix=", then close it" - say " would create the issue, $comment_count comment(s)$suffix" - continue - fi - - # Create any label this issue needs and the target does not have yet. - while read -r label; do - [ -n "$label" ] || continue - if ! grep -qxF "$label" <<<"$existing_labels"; then - jq -n --arg n "$label" '{name: $n, color: "#ededed"}' | - gitea POST "repos/$TARGET_REPO/labels" >/dev/null - existing_labels="$existing_labels"$'\n'"$label" - say " created label: $label" - fi - done < <(jq -r '.[]' <<<"$labels") - - created="$(jq -n --arg t "$title" --arg b "$body" --argjson l "$labels" \ - '{title: $t, body: $b, labels: $l}' | - gitea POST "repos/$TARGET_REPO/issues")" - new_number="$(jq -r '.number' <<<"$created")" - say " -> $GITEA_URL/$TARGET_REPO/issues/$new_number" - - # Comments in order. Each gets the same attribution header as the body; see - # loss (1) in the header of this file. - while read -r comment; do - [ -n "$comment" ] || continue - comment_body="$(jq -r ' - "_@" + .user.login + " am " + (.created_at | split("T")[0]) - + " (migriert von GitHub)._\n\n" + (.body // "")' <<<"$comment")" - jq -n --arg b "$comment_body" '{body: $b}' | - gitea POST "repos/$TARGET_REPO/issues/$new_number/comments" >/dev/null - done < <(gh api --paginate -H "Accept: application/vnd.github+json" \ - "repos/$SOURCE_REPO/issues/$number/comments?per_page=100" | - jq -s -c 'add // [] | .[]') - - if [ "$state" = closed ]; then - jq -n '{state: "closed"}' | - gitea PATCH "repos/$TARGET_REPO/issues/$new_number" >/dev/null - say " closed" - fi - - printf '%s\t%s\n' "$number" "$new_number" >>"$MAP_FILE" -done < <(jq -r '.[].number' <<<"$issues_json") - -say "" -if [ "$DRY_RUN" -eq 1 ]; then - say "dry run finished - $count issue(s) would have been created" -else - say "finished - $count issue(s) created, mapping appended to $MAP_FILE" - say "the mapping is the input for the # rewrite, which this script does not do" -fi