Fix the defects found by the live end-to-end run, add RC-017

- Close stdin for ssh, git, curl and the framework scripts, so a real ssh
  no longer swallows answers meant for later prompts.
- Count LICENSE and LICENSE + README.md (what Gitea creates) as content
  the script made, so a partly created repository can be reused.
- Document the observed token scopes (write:user for user-owned Gitea
  repositories) and the Gitea README.md in the README.
- Correct criterion 2 of MIL-002 (new Proposed version row).
- Record the run and the final security review as RC-017.
- Tests: stdin regression, initial_only reuse, other files count as
  content, example config needs its address edited.

Task: MIL-003#6
Task: MIL-002#1
Refs #20

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 21:41:47 +08:00
co-authored by Claude Sonnet 5.5
parent 0030334e5e
commit 613a288dea
13 changed files with 203 additions and 33 deletions
+20 -13
View File
@@ -15,8 +15,12 @@ and owner (a user or an organization, separately on each host), shows a plan,
and only creates anything after you pass `--apply` and answer yes.
> **Status.** The script is tested with stubbed host APIs and real git against
> local repositories (see [Development](#development)). A first run against
> real GitHub and Gitea repositories is still to be recorded.
> local repositories (see [Development](#development)). A first end-to-end run
> on real GitHub and Gitea repositories, with organization owners on both, has
> passed (2026-10-05, review record RC-017). Creating a repository under your
> own Gitea account needs the `write:user` token scope (see
> [Token permissions](#token-permissions)); that path has not been completed
> yet.
## Contents
@@ -63,7 +67,7 @@ are accepted, and anything else stops the run with a message that names the key
and the line, never the value.
```bash
cp config.env.example config.env # service addresses, not secret
cp config.env.example config.env # service addresses, not secret: set GITEA_URL (and GITEA_API_URL)
cp .env.example .env # credentials: keep private
chmod 600 .env # Linux and macOS
```
@@ -195,7 +199,7 @@ repositories for the chosen owner and to push to the new one.
| Create a private repository | classic token with the `repo` scope | GitHub REST documentation, "Create a repository" |
| Create a public repository only | classic token with `public_repo` is enough | same |
| Push from the Gitea mirror | covered by `repo` | |
| Check that you belong to the organization owner | probably `read:org` | **Not confirmed**: the GitHub documentation names no scope for this call. If the script says you do not belong to an organization that you do belong to, add `read:org`. |
| Check that you belong to the organization owner | worked with a classic token that has `repo` and `admin:org` | `read:org` alone was **not tested**: the GitHub documentation names no scope for this call. If the script says you do not belong to an organization that you do belong to, add `read:org`. |
- **Organization owners:** you must be an active member who is allowed to
create repositories in the organization. Organizations that require SSO or
@@ -211,9 +215,9 @@ repositories for the chosen owner and to push to the new one.
| Need | Scope | Source |
| --- | --- | --- |
| Read the account the token belongs to | `read:user` | Gitea documentation |
| Create repositories, manage the push mirror | `write:repository` | Gitea documentation |
| Look up an organization and your permissions in it | `read:organization` | Gitea documentation |
| Create a repository in an organization | probably `write:organization` as well | **Not confirmed**: expected from how the Gitea API groups organization calls; to be confirmed in the first end-to-end run. |
| Create a repository **under your own account** | `write:user` | **Confirmed by a real server**: without it Gitea answers `required=[write:user]` |
| Create a repository in an organization, manage its push mirror | `write:organization` and `write:repository` | worked with a token that has both, plus `read:user`; the minimum was not narrowed down |
| Look up an organization and your permissions in it | covered by the scopes above | worked in the end-to-end run |
A missing scope shows up as an HTTP 403 with the server's own message. The
script stops before it creates anything when a preflight check is refused.
@@ -256,7 +260,7 @@ step, `2` a usage error.
| A tool, a config key or a token is missing or invalid | stops before any request | fix it and run again |
| A token is refused, an owner is unknown, a name is taken, the license is missing | stops in the preflight; nothing was created | fix the cause |
| The host cannot be reached | stops with the host name | try again |
| A repository already exists and is empty (Gitea: or holds only the license) | offers to reuse it (default no) | answer, or choose another name |
| A repository already exists and is empty (Gitea: or holds only the license and the README Gitea adds) | offers to reuse it (default no) | answer, or choose another name |
| A repository already has content | stops | choose another name or remove it |
| A step fails after another succeeded | stops and prints what exists, what failed and how to continue | fix the cause and run the **same command again with `--apply`**: what was created is offered for reuse |
| The mirror is refused (disabled, interval too short) | keeps the repositories and reports it | change `MIRROR_INTERVAL` or ask the Gitea administrator, then run again |
@@ -285,17 +289,20 @@ web interface and the project directory by hand.
this, and revoke it if the Gitea server is ever in doubt.
- **`sync_on_commit` may be ignored.** When a push mirror is created through
the API, some Gitea versions ignore `sync_on_commit` (upstream issue
go-gitea/gitea#22990). The script reads the mirror back and warns if the
setting was not applied; the mirror then syncs on its interval
go-gitea/gitea#22990). On Gitea 1.27.3 it was applied: a branch pushed to
Gitea reached GitHub within seconds. The script reads the mirror back and
warns if the setting was not applied; the mirror then syncs on its interval
(`MIRROR_INTERVAL`, default 10 minutes). The first sync is requested right
after the mirror is created.
- **The server decides the shortest interval** and whether push mirrors are
allowed at all. A refused mirror stops the run with the server's message;
the repositories created so far are kept.
- **The license commit.** Gitea adds the license file when the repository is
created with `auto_init`. The script sends no README, so the repository
should hold only `LICENSE`; this is still to be confirmed against a real
server.
created with `auto_init`, and on the real server it also adds a generated
`README.md`. Both are mirrored to GitHub and become the first commit of the
local project. A Gitea repository that holds only these files counts as
content this script created and is offered for reuse; a repository with
anything else counts as having content and is refused.
- **No rollback.** See [Error handling and recovery](#error-handling-and-recovery).
- **Mirror direction is Gitea to GitHub only.** Push to Gitea; GitHub is a
copy.