@caddis/cli 0.4.18 → 0.4.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/scripts/caddis_adhoc.py +530 -0
  3. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +8 -0
  4. package/bundles/antigravity-plugin/skills/_registry.md +5 -0
  5. package/bundles/antigravity-plugin/skills/data-lineage/SKILL.md +32 -0
  6. package/bundles/antigravity-plugin/skills/hld/SKILL.md +39 -0
  7. package/bundles/antigravity-plugin/skills/lld/SKILL.md +45 -0
  8. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +87 -0
  9. package/bundles/antigravity-plugin/skills/port-to-github/SKILL.md +231 -0
  10. package/bundles/antigravity-plugin/skills/port-to-github/references/ci-and-credentials.md +66 -0
  11. package/bundles/antigravity-plugin/skills/port-to-github/references/config.example.json +18 -0
  12. package/bundles/antigravity-plugin/skills/port-to-github/references/old-host.md +61 -0
  13. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
  14. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
  15. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Get-PortSource.ps1 +459 -0
  16. package/bundles/antigravity-plugin/skills/port-to-github/scripts/PortCommon.ps1 +130 -0
  17. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
  18. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
  19. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
  20. package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
  21. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  22. package/bundles/antigravity-plugin-extras/skills/_registry.md +3 -0
  23. package/bundles/antigravity-plugin-extras/skills/data-lineage/SKILL.md +158 -0
  24. package/bundles/antigravity-plugin-extras/skills/data-lineage/scripts/convert_doc_to_confluence.py +1430 -0
  25. package/bundles/antigravity-plugin-extras/skills/hld/SKILL.md +220 -0
  26. package/bundles/antigravity-plugin-extras/skills/hld/references/gold-standard-hld.md +428 -0
  27. package/bundles/antigravity-plugin-extras/skills/hld/scripts/convert_doc_to_confluence.py +1430 -0
  28. package/bundles/antigravity-plugin-extras/skills/lld/SKILL.md +198 -0
  29. package/bundles/antigravity-plugin-extras/skills/lld/scripts/convert_doc_to_confluence.py +1430 -0
  30. package/bundles/codex/.codex/skills/devops/port-to-github/SKILL.md +231 -0
  31. package/bundles/codex/.codex/skills/devops/port-to-github/references/ci-and-credentials.md +66 -0
  32. package/bundles/codex/.codex/skills/devops/port-to-github/references/config.example.json +18 -0
  33. package/bundles/codex/.codex/skills/devops/port-to-github/references/old-host.md +61 -0
  34. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Compare-PortRefs.ps1 +68 -0
  35. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Convert-PortWorkflow.ps1 +92 -0
  36. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Get-PortSource.ps1 +459 -0
  37. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/PortCommon.ps1 +130 -0
  38. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Publish-PortToGitHub.ps1 +156 -0
  39. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Switch-CheckoutOrigin.ps1 +182 -0
  40. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortReadiness.ps1 +218 -0
  41. package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortWorkflowParses.ps1 +62 -0
  42. package/bundles/manifest.json +3 -3
  43. package/package.json +1 -1
@@ -0,0 +1,231 @@
1
+ ---
2
+ name: port-to-github
3
+ description: Move (port) an app or repository to GitHub from ANY source - a Gitea/GitLab/any git server URL, a local folder, a network (UNC) share, or a folder on a remote Windows box reached over WinRM - with or without existing git history. Keeps every branch and tag, verifies by SHA, translates Gitea/Forgejo Actions CI, switches deploy checkouts to GitHub with automatic rollback, and retires the old host safely. Use when the user says "move this app to GitHub", "port X to GitHub", "migrate from Gitea", "put this folder on GitHub", "this app only exists on the server", "import the repo from the prod box", or "cut the deploy over to GitHub".
4
+ ---
5
+
6
+ # port-to-github
7
+
8
+ Move one app to GitHub without losing history, leaking a secret, or breaking its deploy.
9
+
10
+ The work is in **six phases**. Each ends in a check that can FAIL. Do not start a phase until
11
+ the one before it passed. Scripts are in `scripts/` next to this file. They are PowerShell 7
12
+ (`pwsh`); the parts that run on a remote box are Windows PowerShell 5.1 compatible.
13
+
14
+ | Phase | Script | Changes anything outside this machine? |
15
+ |---|---|---|
16
+ | 1. Stage the source | `Get-PortSource.ps1` | No. The source is only read. |
17
+ | 2. Readiness | `Test-PortReadiness.ps1` | No. |
18
+ | 3. Publish | `Publish-PortToGitHub.ps1` | **Yes: creates a GitHub repo and pushes.** |
19
+ | 4. CI | `Convert-PortWorkflow.ps1`, `Test-PortWorkflowParses.ps1` | Yes: a feature branch and a PR. |
20
+ | 5. Cutover | `Switch-CheckoutOrigin.ps1` | **Yes: changes where a deploy host pulls from.** |
21
+ | 6. Retire the old host | manual, see `references/old-host.md` | **Yes, and can be destructive.** |
22
+
23
+ **Stop and ask the user before phases 3, 5 and 6.** Each one is outward-facing. Say exactly
24
+ what will be created or changed, on which host, and how to undo it.
25
+
26
+ Not every port needs every phase. A folder that never had CI or a deploy host ends after
27
+ phase 3. Say which phases apply and why, before you start.
28
+
29
+ ## Before you start
30
+
31
+ 1. **Read the project config**, if one exists: `.caddis/port-to-github.json` in the repo you are
32
+ working from (or `$env:PORT_TO_GITHUB_CONFIG`). It sets the GitHub `owner`, `visibility`,
33
+ staging folder, the name for the old remote, dependency hosts and their secrets, and
34
+ `projectNotes` - pointers to the project's own rules. **Read every file `projectNotes` names
35
+ before phase 4.** Project rules beat this skill. The shape is in `references/config.example.json`.
36
+ 2. `gh auth status` must show an account that can create repos in the owner. `git` must be able
37
+ to push to github.com (run `gh auth setup-git` once if not).
38
+ 3. **Find the authoritative copy.** If a folder is a clone of a server, port from the SERVER
39
+ URL, not the folder: the folder may lack branches the server has. Port from a folder only
40
+ when it is the only copy, or when it holds commits the server lacks. Phase 1 reports both
41
+ cases.
42
+
43
+ ## Phase 1 - stage the source
44
+
45
+ ```powershell
46
+ $s = '<path-to-this-skill>/scripts'
47
+ & $s/Get-PortSource.ps1 -Source <source> [-Name <repo-name>] [-IncludeUncommitted]
48
+ ```
49
+
50
+ | Source | Example `-Source` |
51
+ |---|---|
52
+ | any git server | `https://gitea.example/org/app.git`, `git@host:org/app.git` |
53
+ | local folder | `D:\work\app` |
54
+ | network share | `\\fileserver\projects\app` |
55
+ | remote box (WinRM) | `winrm://appserver01/D:/apps/app` (or `-ComputerName appserver01 -Source D:\apps\app`) |
56
+
57
+ What it does, for every kind: builds `<staging>/<Name>/mirror.git` holding ONLY branches and
58
+ tags, removes the remote so nothing can be pushed back to the source, and writes
59
+ `manifest.json`. A folder without history becomes one import commit; `node_modules`, virtual
60
+ envs and caches are skipped, and a `.gitignore` is written if there was none.
61
+
62
+ Read the output. These need a decision, not a shrug:
63
+
64
+ - **Uncommitted changes** in a source checkout are left out unless you pass
65
+ `-IncludeUncommitted`, which puts them on a separate branch `port/uncommitted-snapshot-*`.
66
+ The real branches stay exactly as committed. Ask the user which they want.
67
+ - **Remote-only branches**: branches the checkout only knows as `origin/x`. If the checkout is
68
+ the only copy, re-run with `-PromoteRemoteBranches origin`. If a server has them, port from it.
69
+ - **Likely secrets** in a plain folder or a snapshot stop the script (exit 2). Nothing has left
70
+ the machine. Show the user the list. Re-run with `-OnSecret Exclude` only after they agree
71
+ those files must not be published.
72
+ - **Dropped refs** (`refs/pull/*`, `refs/remotes/*`, notes) are expected. GitHub refuses
73
+ `refs/pull/*`, and pull-request discussion does not move with git anyway. If the old host's
74
+ PR and issue history matters, export it before phase 6.
75
+
76
+ ## Phase 2 - readiness
77
+
78
+ ```powershell
79
+ & $s/Test-PortReadiness.ps1 -Name <repo-name>
80
+ ```
81
+
82
+ **Blockers (exit 1) must be fixed before phase 3:** a file over 100 MB anywhere in history,
83
+ Git LFS content, a likely live credential in the current tree, an empty `${{ }}` in a file
84
+ under `.github/workflows`. A credential found in the tree must be **rotated**, not only
85
+ deleted: history keeps it. (An empty `${{ }}` in a `.gitea/` workflow is only a warning here:
86
+ GitHub does not read that folder, so it matters in phase 4.)
87
+
88
+ **Branches that already contain `.github/workflows` run them the moment they are pushed** -
89
+ deploy jobs included. Readiness lists those branches; publish refuses them unless you pass
90
+ `-AllowWorkflowRuns`, which you do only after reading every such workflow with the user.
91
+
92
+ **Warnings are decisions.** Go through each with the user: secret-type files in past history
93
+ (the new repo will expose them), CI credentials, dependency hosts, runner labels, files that
94
+ still name the old host. The report is in `readiness.json`.
95
+
96
+ ## Phase 3 - publish (ask first)
97
+
98
+ Tell the user: the repo name, owner, visibility, branch and tag counts. Then:
99
+
100
+ ```powershell
101
+ & $s/Publish-PortToGitHub.ps1 -Name <repo-name> [-Owner <org>] [-Visibility private]
102
+ ```
103
+
104
+ It first checks the stage still holds exactly the refs phase 1 recorded (anything else would be
105
+ pushed and then "verified" against itself), creates the repo EMPTY, pushes `refs/heads/*` and
106
+ `refs/tags/*` with an explicit refspec, sets the default branch, and compares every ref by SHA
107
+ (exit 1 on any difference). Do not run other git commands inside the stage; if you did,
108
+ rebuild it with `Get-PortSource.ps1 -Force`.
109
+
110
+ - **Never** `git push --mirror`: it tries to push hidden refs, fails AFTER pushing the rest (so
111
+ a populated repo reports failure), and deletes anything on GitHub the stage lacks.
112
+ - **An existing repo.** If one exists with history, the script refuses. Find out whether it
113
+ shares history with the source: for a few of its commit SHAs, `git -C mirror.git cat-file -e
114
+ <sha>^{commit}`. If it shares nothing (for example a snapshot someone uploaded with a fresh
115
+ "Initial commit"), pushing on top welds a fake ancestry in forever: save anything of value
116
+ from it, delete it, and re-run. Use `-AllowExisting` only when it is a genuine older copy; the
117
+ script then requires every GitHub branch to be an ancestor of the staged one.
118
+ - Verify with the SHA comparison, never by counting refs or by "the push said OK".
119
+
120
+ Stop here if the app has no CI and no deploy host. Otherwise continue.
121
+
122
+ ## Phase 4 - CI
123
+
124
+ Work in a normal clone of the NEW repo, on a feature branch.
125
+
126
+ ```powershell
127
+ git clone https://github.com/<owner>/<repo>.git; cd <repo>; git switch -c port/github-actions
128
+ & $s/Convert-PortWorkflow.ps1 -AddPermissions [-SourceRef <sha>]
129
+ ```
130
+
131
+ It copies `.gitea/workflows` (or `.forgejo/workflows`) to `.github/workflows`, rewrites the
132
+ `gitea.` context to `github.`, and prints every credential line. **Leave the scripts the
133
+ workflows call where they are** - GitHub only needs the workflow file under `.github/`.
134
+
135
+ Then do by hand, one line at a time - this is the whole job, and a blanket swap breaks it:
136
+
137
+ 1. **Classify every credential line.** It faces ORIGIN (checkout, fetch, reset, pushing a tag
138
+ back to this repo) or it names a DEPENDENCY host (a package registry, a library pinned from
139
+ another repo or server). There is no third category.
140
+ - ORIGIN -> `${{ github.token }}`, username `x-access-token`. This includes an
141
+ `actions/checkout` that pushes tags - it is easy to miss.
142
+ - DEPENDENCY -> a secret valid for THAT host. `github.token` reads only the repo whose
143
+ workflow is running, so a library in ANOTHER GitHub repo needs its own read token.
144
+ One job can hold two credential pairs that serve different hosts. Change only the pair
145
+ that faces origin.
146
+ 2. **One Authorization header per host.** `http.<url>.extraheader` values ACCUMULATE; two for the
147
+ same host is an HTTP 400. `actions/checkout` persists its own header, so a step that adds
148
+ another for github.com must unset the checkout's for that command and restore it after.
149
+ 3. **npm and GitHub dependencies:** npm fetches a GitHub dependency over SSH even when
150
+ `package.json` says `https`. Set `url.https://github.com/.insteadOf=ssh://git@github.com/`
151
+ and `GIT_TERMINAL_PROMPT=0` so a missing credential fails in seconds instead of hanging.
152
+ 4. **Runner labels** must exist on GitHub (`runs-on:`). Self-hosted runners must be registered
153
+ to the org or repo, not only to the old host.
154
+ 5. **Never write `${{ }}` in any text in a workflow**, including comments inside `run:` blocks.
155
+ 6. **If a workflow was frozen** on the old host (triggers reduced to `workflow_dispatch`), pass
156
+ `-SourceRef` with the last commit before the freeze. A workflow built from a frozen copy
157
+ fires on nothing, which looks like "still queued", not like a fault.
158
+
159
+ Details and the credential patterns: `references/ci-and-credentials.md`.
160
+
161
+ **Validate before anything reaches `main`.** Commit the workflow files BY NAME, push the branch,
162
+ then:
163
+
164
+ ```powershell
165
+ & $s/Test-PortWorkflowParses.ps1 -Owner <owner> -Repo <repo> -Branch port/github-actions
166
+ ```
167
+
168
+ An unparseable workflow produces a failed run with zero jobs even on a branch its triggers
169
+ ignore; a valid one produces no run (or a normal run). A broken workflow on `main` leaves the
170
+ app with no deploy path, so this check is not optional. Then open a PR and let the real jobs
171
+ run; merging follows the repo's normal rules.
172
+
173
+ ## Phase 5 - cutover (ask first)
174
+
175
+ Only for apps with a deploy host whose checkout pulls from the old origin. Order matters:
176
+
177
+ 1. `Switch-CheckoutOrigin.ps1 -Mode Inspect` on each host. Record what it prints.
178
+ 2. **Tell the user** the host, path, old and new URL, and the rollback command.
179
+ 3. Switch:
180
+ ```powershell
181
+ & $s/Switch-CheckoutOrigin.ps1 -Path D:\apps\app [-ComputerName appserver01] -Mode Switch `
182
+ -NewUrl https://github.com/<owner>/<repo>.git -VerifyWithGhToken
183
+ ```
184
+ It records the old URL first, keeps it as a second remote, fetches, and requires the
185
+ checkout's HEAD to be on a GitHub branch. On failure it restores the old origin itself.
186
+ `-VerifyWithGhToken` lends your gh token to that one fetch, in memory only, because a deploy
187
+ host normally has no GitHub credential of its own (CI supplies one per run).
188
+ 4. **Stop the old host from deploying** (disable its deploy job) only AFTER the first GitHub
189
+ deploy is green. Two schedulers on one checkout have no lock between them.
190
+ 5. **Verify the deploy by an artefact changing** - a built bundle's hash or file time - never by
191
+ an HTTP 200 or a version endpoint alone. Both pass when a new backend serves an old frontend.
192
+
193
+ Rollback, at any point before phase 6:
194
+
195
+ ```powershell
196
+ & $s/Switch-CheckoutOrigin.ps1 -Path D:\apps\app [-ComputerName appserver01] -Mode Rollback
197
+ ```
198
+
199
+ Also repoint developer checkouts the same way (`-Mode Switch` without `-VerifyWithGhToken`), so
200
+ `git push` goes to GitHub. Keep the old remote; it is the local rollback.
201
+
202
+ ## Phase 6 - retire the old host (ask first)
203
+
204
+ Read `references/old-host.md`. The short version:
205
+
206
+ - Prove nothing is lost first:
207
+ `& $s/Compare-PortRefs.ps1 -Left <old-url> -Right <github-url> -Mode LeftInRight` must exit 0.
208
+ Push any ref that exists only on the old host before you continue.
209
+ - Prefer a **read-only pull mirror** of GitHub on the old host (Actions off) over deleting it:
210
+ anything still pinned to the old URL keeps working. Never a push mirror from CI.
211
+ - Anything that still pins a dependency from the old host keeps the old host alive. Find those
212
+ pins before retiring it.
213
+
214
+ ## Report
215
+
216
+ End with a short table per app: source kind, branches and tags ported (SHA-verified yes/no),
217
+ warnings accepted and by whom, CI state, cutover state and rollback command, what is left.
218
+ Say which phases were skipped and why.
219
+
220
+ ## Rules that apply to every phase
221
+
222
+ - Stage files BY NAME. The only `git add -A` is inside the scripts, on fresh throwaway trees.
223
+ - A check that cannot fail is not a check. Every script here exits non-zero on the failure it
224
+ exists for; do not pipe it to `Out-Null` or read only its last line.
225
+ - Audit the default branch on the SERVER (`origin/main`), never a working tree that may sit on
226
+ an old feature branch.
227
+ - Code that runs on a remote box runs under Windows PowerShell 5.1. There, with
228
+ `$ErrorActionPreference='Stop'`, a native command's stderr is a terminating error even with
229
+ `2>$null`. Use `'Continue'` and judge native commands by `$LASTEXITCODE`.
230
+ - One variable at a time. When a whole batch fails the same way, change one thing and retry
231
+ one item before re-reading the request.
@@ -0,0 +1,66 @@
1
+ # CI and credentials after a port
2
+
3
+ Every item here broke a real deploy at least once.
4
+
5
+ ## The two kinds of credential
6
+
7
+ | Faces | Examples | On GitHub |
8
+ |---|---|---|
9
+ | ORIGIN - this repo | `actions/checkout`, a deploy script's `git fetch`/`reset`, pushing a version tag back | `${{ github.token }}`, username `x-access-token`. Needs `permissions: contents: write` to push. Nothing to store or rotate. |
10
+ | DEPENDENCY - another host or repo | a library pinned `git+https://host/org/lib.git@v1`, a private npm/PyPI registry, a Git server that stays behind | A secret that can read THAT host. `github.token` cannot read a different repo, even in the same org. |
11
+
12
+ Choose the credential **per host**, from the host named in the pin, never from where the app is
13
+ hosted. When a pin moves from the old server to GitHub, its credential must move with it in the
14
+ same commit; when an app moves but its pins do not, the old credential must stay.
15
+
16
+ A deploy script that builds its auth header from `git remote get-url origin` needs NO change
17
+ for the origin half: repointing origin (phase 5) is the whole cutover. It DOES need a change if
18
+ it also uses that origin-derived credential for dependency clones - the moment origin is
19
+ GitHub and the dependency is not, that pairing is wrong.
20
+
21
+ ## Injecting a credential for a child process (pip, npm)
22
+
23
+ `GIT_CONFIG_*` environment entries (git 2.31+) are inherited by every git a tool spawns:
24
+
25
+ ```yaml
26
+ env:
27
+ GIT_CONFIG_COUNT: '2'
28
+ GIT_CONFIG_KEY_0: http.https://github.com/.extraheader
29
+ GIT_CONFIG_VALUE_0: AUTHORIZATION: basic <base64 of x-access-token:TOKEN>
30
+ GIT_CONFIG_KEY_1: http.https://old-host.example/.extraheader
31
+ GIT_CONFIG_VALUE_1: AUTHORIZATION: basic <base64 of user:OLD_HOST_TOKEN>
32
+ ```
33
+
34
+ Add a new entry and increment the count. Never replace entry 0 when another host still needs
35
+ it. Build the base64 in a script step from `env:`; never put a token in a URL - it lands in
36
+ lockfiles and logs.
37
+
38
+ ## Traps
39
+
40
+ - **Header accumulation.** `http.<url>.extraheader` never overrides; git sends every matching
41
+ one. `actions/checkout` persists one in `.git/config`. A second for the same host -> HTTP 400
42
+ `Duplicate header: "Authorization"`. Unset the checkout's entry for the one command that
43
+ needs a different credential, then restore it; otherwise every later step loses its auth.
44
+ - **`git config --local` outside a repo fails.** A runner's temp working directory is not a
45
+ repository. Check `git rev-parse --is-inside-work-tree` first.
46
+ - **npm uses SSH for GitHub deps** despite `https` in `package.json` and the lockfile. Add
47
+ `url.https://github.com/.insteadOf ssh://git@github.com/`. Also make sure the lockfile's
48
+ `resolved` URLs say `git+https`, not `git+ssh`.
49
+ - **A cache hit is not proof of credentials.** A warm npm or pip cache skips the authenticated
50
+ fetch entirely. Prove a new credential with a fresh install (cleared cache, empty `HOME`, no
51
+ credential helper).
52
+ - **Windows PowerShell 5.1 runners.** With `$ErrorActionPreference='Stop'`, anything a native
53
+ exe writes to stderr becomes a terminating error, and `2>$null` does not prevent it. Use
54
+ `'Continue'` in the step, `2>&1 | Out-Null` to silence, and judge by `$LASTEXITCODE`.
55
+ - **A step whose last native command is EXPECTED to fail** (a negative test) must end with
56
+ `exit 0`, or the step is red after printing its pass.
57
+ - **Required checks.** A job skipped by a path filter at the TRIGGER never reports, so making it
58
+ a required check blocks every PR that does not touch those paths. A job skipped by `if:` counts
59
+ as passed. Renaming a job renames its check context and silently breaks a branch rule.
60
+ - **`concurrency: cancel-in-progress`** cancels a PR's running jobs when you push again. A
61
+ `cancelled` job or a PR with no runs is often this, not a fault. Re-check against the new
62
+ head SHA.
63
+ - **Deploy scripts may run from the deploy checkout**, not from the commit being deployed. If the
64
+ workflow syncs that checkout to the target commit before calling the script, a script change
65
+ takes effect on the first deploy; if it does not, it takes effect on the NEXT deploy. Check
66
+ which before predicting.
@@ -0,0 +1,18 @@
1
+ {
2
+ "_comment": "Copy to .caddis/port-to-github.json in the repo you run the port from. Every field is optional.",
3
+ "owner": "example-org",
4
+ "visibility": "private",
5
+ "stagingRoot": "%TEMP%\\port-to-github",
6
+ "oldRemoteName": "gitea",
7
+ "dependencyHosts": [
8
+ { "host": "github.com", "username": "x-access-token", "secret": "ORG_READ_TOKEN", "note": "libraries in other repos of the org" },
9
+ { "host": "git.example.internal", "username": "ci-bot", "secret": "OLD_HOST_READ_TOKEN", "note": "stays until nothing pins from it" }
10
+ ],
11
+ "deployHosts": [
12
+ { "computerName": "appserver01", "checkoutRoot": "D:\\apps" }
13
+ ],
14
+ "projectNotes": [
15
+ "AGENTS.md - the project's own rules; they override this skill",
16
+ "docs/runbooks/<your-migration-runbook>.md"
17
+ ]
18
+ }
@@ -0,0 +1,61 @@
1
+ # Retiring the old host
2
+
3
+ Only after: the app is on GitHub, SHA-verified, and its first GitHub deploy was green and
4
+ verified by an artefact changing.
5
+
6
+ ## 1. Prove nothing is lost
7
+
8
+ ```powershell
9
+ & <skill>/scripts/Compare-PortRefs.ps1 -Left <old-url> -Right https://github.com/<owner>/<repo>.git `
10
+ -Mode LeftInRight -LeftLabel old -RightLabel github
11
+ ```
12
+
13
+ Exit 0 means every branch and tag on the old host exists on GitHub with the same SHA. If not,
14
+ push the missing refs to GitHub first (from a fresh `git clone --mirror` of the old host, with
15
+ an explicit refspec for just those refs) and run it again. Release tags pushed by the old CI
16
+ after the port are the usual stragglers.
17
+
18
+ ## 2. Find what still depends on the old host
19
+
20
+ Search every consumer's default branch (not working trees) for the old host name: dependency
21
+ pins, registry config, CI steps, deploy scripts. Anything found keeps the old host alive, and
22
+ keeps a credential for it alive.
23
+
24
+ ## 3. Choose: read-only mirror, archive, or delete
25
+
26
+ | Option | When | Cost |
27
+ |---|---|---|
28
+ | **Read-only pull mirror of GitHub** (Actions OFF) | Something still pins from the old URL, or you want a live fallback | The old host keeps running; a mirror that stops pulling goes stale silently |
29
+ | Archive (read-only, no sync) | Nothing depends on it; you want the old PR/issue history browsable | A frozen snapshot that drifts from day one |
30
+ | Delete | Nothing depends on it and its history is exported | Irreversible |
31
+
32
+ **Never a push mirror driven by CI.** A job pushing into a host that then runs CI on what
33
+ arrives has caused a full outage: tests failed, deploy was skipped (not failed), the push guard
34
+ only checked `failure`, and it pushed a change disabling the old deploy - leaving no deploy path
35
+ anywhere. A pull mirror with Actions disabled cannot do that.
36
+
37
+ ## 4. Converting a Gitea repo into a pull mirror
38
+
39
+ An existing Gitea repo cannot be converted in place. It must be deleted and recreated as a
40
+ mirror under the same path, which **destroys its pull requests and issues** - export them first
41
+ (API: `/repos/{owner}/{repo}/pulls?state=all`, `/issues?state=all`, comments per item).
42
+
43
+ 1. Run step 1 again immediately before, and export PRs/issues.
44
+ 2. `DELETE /api/v1/repos/{owner}/{repo}`.
45
+ 3. **Verify the delete on BOTH halves: the API returns 404 AND the repository directory is gone
46
+ on disk.** Gitea can remove the database row and fail on disk (a locked pack file). The next
47
+ create then returns 500 while still making an EMPTY mirror row, on which `mirror-sync`
48
+ returns 400. The Gitea log names the locked file; nothing else does.
49
+ 4. `POST /api/v1/repos/migrate` with `mirror: true`, the GitHub clone URL, and a GitHub read
50
+ token (`auth_token`). Then disable Actions on it (`has_actions: false`).
51
+ 5. Verify by refs, not by existence: `Compare-PortRefs.ps1 -Left <github> -Right <gitea> -Mode Equal`.
52
+ 6. If something pins from the mirror, prove it: clone the exact pinned tag from the mirror.
53
+
54
+ ## 5. Watching a mirror
55
+
56
+ A pull mirror syncs on an interval (Gitea default 8h). A mirror behind GitHub is either LAGGING
57
+ (the GitHub commit is newer than the mirror's last pull - normal) or STALE (the commit was there
58
+ at the last pull and still did not arrive - the stored credential probably expired). Decide
59
+ which by comparing the GitHub commit time with the mirror's last-update time before you touch
60
+ any credential. `POST /repos/{owner}/{repo}/mirror-sync` forces a pull: an immediate match means
61
+ it was lag.
@@ -0,0 +1,68 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Compare branches and tags between two git locations by SHA. The only honest "did it copy" check.
4
+
5
+ .DESCRIPTION
6
+ -Left and -Right can each be a URL, a local path, a UNC path or a bare repository.
7
+ Uses git ls-remote, so nothing is fetched or changed.
8
+
9
+ Modes:
10
+ Equal every head and tag matches on both sides (after a push)
11
+ LeftInRight every Left ref exists in Right with the same SHA; Right may have more
12
+ (before destroying an old host: nothing on the old host may be lost)
13
+
14
+ Exit 0 when the mode holds, 1 when it does not, and the differing refs are printed.
15
+ #>
16
+ [CmdletBinding()]
17
+ param(
18
+ [Parameter(Mandatory)][string]$Left,
19
+ [Parameter(Mandatory)][string]$Right,
20
+ [ValidateSet('Equal', 'LeftInRight')][string]$Mode = 'Equal',
21
+ [string]$LeftLabel = 'left',
22
+ [string]$RightLabel = 'right',
23
+ [switch]$PassThru
24
+ )
25
+
26
+ $ErrorActionPreference = 'Stop'
27
+ . (Join-Path $PSScriptRoot 'PortCommon.ps1')
28
+
29
+ function Get-Refs([string]$Where) {
30
+ $lines = Invoke-PortGit @('ls-remote', '--heads', '--tags', $Where)
31
+ $map = @{}
32
+ foreach ($l in $lines) {
33
+ if (-not $l) { continue }
34
+ $sha, $ref = $l -split "`t", 2
35
+ # An annotated tag shows twice: the tag object and the peeled commit (^{}). Compare the
36
+ # tag object; the peeled line is derived from it.
37
+ if ($ref -like '*^{}') { continue }
38
+ $map[$ref] = $sha
39
+ }
40
+ return $map
41
+ }
42
+
43
+ $l = Get-Refs $Left
44
+ $r = Get-Refs $Right
45
+
46
+ $onlyLeft = @($l.Keys | Where-Object { -not $r.ContainsKey($_) } | Sort-Object)
47
+ $onlyRight = @($r.Keys | Where-Object { -not $l.ContainsKey($_) } | Sort-Object)
48
+ $differ = @($l.Keys | Where-Object { $r.ContainsKey($_) -and $r[$_] -ne $l[$_] } | Sort-Object)
49
+
50
+ $ok = ($onlyLeft.Count -eq 0 -and $differ.Count -eq 0)
51
+ if ($Mode -eq 'Equal') { $ok = $ok -and ($onlyRight.Count -eq 0) }
52
+
53
+ Write-Host ("{0}: {1} refs {2}: {3} refs only-{0}: {4} only-{2}: {5} differ: {6}" -f $LeftLabel, $l.Count, $RightLabel, $r.Count, $onlyLeft.Count, $onlyRight.Count, $differ.Count)
54
+ foreach ($x in $onlyLeft) { Write-Host " only in ${LeftLabel}: $x" }
55
+ if ($Mode -eq 'Equal') { foreach ($x in $onlyRight) { Write-Host " only in ${RightLabel}: $x" } }
56
+ foreach ($x in $differ) { Write-Host " differs: $x $($l[$x].Substring(0,12)) vs $($r[$x].Substring(0,12))" }
57
+
58
+ if ($l.Count -eq 0) {
59
+ Write-PortFail "$LeftLabel has no refs at all. An empty side cannot prove anything."
60
+ $ok = $false
61
+ }
62
+ if ($ok) { Write-PortOk "Refs agree ($Mode)." } else { Write-PortFail "Refs do NOT agree ($Mode)." }
63
+
64
+ if ($PassThru) {
65
+ [pscustomobject]@{ Ok = $ok; OnlyLeft = $onlyLeft; OnlyRight = $onlyRight; Differ = $differ; LeftCount = $l.Count; RightCount = $r.Count }
66
+ }
67
+ if (-not $ok) { exit 1 }
68
+ exit 0
@@ -0,0 +1,92 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Copy Gitea/Forgejo Actions workflows to .github/workflows in a working clone, and lint them.
4
+
5
+ .DESCRIPTION
6
+ Run inside a normal (non-bare) clone of the NEW GitHub repository, on a feature branch.
7
+ - Reads each workflow from -SourceRef (default HEAD), NOT from the working tree, so you can
8
+ point it at the last commit before a workflow was frozen (e.g. -SourceRef <sha>).
9
+ - Rewrites the `gitea.` expression context to `github.` (same meaning on both).
10
+ - Optionally adds top-level `permissions: contents: write` (needed to push tags).
11
+ - Never touches scripts the workflows call. GitHub only needs the WORKFLOW under
12
+ .github/workflows; leaving .gitea/scripts where it is keeps the diff small.
13
+ - Never stages or commits. It prints the files it wrote so you can review and add them by name.
14
+
15
+ Exit 1 if any written workflow still contains an empty expression.
16
+ #>
17
+ [CmdletBinding()]
18
+ param(
19
+ [string]$RepoPath = '.',
20
+ [string]$From,
21
+ [string]$SourceRef = 'HEAD',
22
+ [switch]$AddPermissions,
23
+ [switch]$Force
24
+ )
25
+
26
+ $ErrorActionPreference = 'Stop'
27
+ . (Join-Path $PSScriptRoot 'PortCommon.ps1')
28
+
29
+ $RepoPath = (Resolve-Path -LiteralPath $RepoPath).Path
30
+ if (-not $From) {
31
+ foreach ($c in '.gitea/workflows', '.forgejo/workflows') {
32
+ $ls = Invoke-PortGit -AllowFailure @('-C', $RepoPath, 'ls-tree', '--name-only', "${SourceRef}:$c")
33
+ if ($ls.ExitCode -eq 0 -and ($ls.Output -join '')) { $From = $c; break }
34
+ }
35
+ }
36
+ if (-not $From) { throw "No .gitea/workflows or .forgejo/workflows at $SourceRef. Pass -From." }
37
+
38
+ $names = @(Invoke-PortGit @('-C', $RepoPath, 'ls-tree', '--name-only', "${SourceRef}:$From") | Where-Object { $_ -match '\.ya?ml$' })
39
+ if ($names.Count -eq 0) { throw "No workflow files in $From at $SourceRef." }
40
+
41
+ $target = Join-Path $RepoPath '.github/workflows'
42
+ New-Item -ItemType Directory -Force -Path $target | Out-Null
43
+ $written = @()
44
+ $bad = 0
45
+
46
+ foreach ($n in $names) {
47
+ $dest = Join-Path $target $n
48
+ if ((Test-Path -LiteralPath $dest) -and -not $Force) { Write-PortWarn "$n already exists in .github/workflows. Skipped (use -Force)."; continue }
49
+ $text = (Invoke-PortGit @('-C', $RepoPath, 'show', "${SourceRef}:$From/$n")) -join "`n"
50
+
51
+ $ctx = 0
52
+ foreach ($m in [regex]::Matches($text, '\$\{\{[^}]*\}\}')) { $ctx += ([regex]::Matches($m.Value, '\bgitea\.')).Count }
53
+ $text = [regex]::Replace($text, '(\$\{\{[^}]*?)\bgitea\.', '$1github.')
54
+ # Repeat until no gitea. context remains inside any expression (several per expression).
55
+ while ([regex]::IsMatch($text, '\$\{\{[^}]*?\bgitea\.')) { $text = [regex]::Replace($text, '(\$\{\{[^}]*?)\bgitea\.', '$1github.') }
56
+
57
+ if ($AddPermissions -and $text -notmatch '(?m)^permissions:') {
58
+ # Instance Replace with a real count. The static [regex]::Replace(s, p, r, 1) binds the 1 to
59
+ # RegexOptions.IgnoreCase and replaces EVERY match.
60
+ if ($text -match '(?m)^jobs:') { $text = ([regex]'(?m)^jobs:').Replace($text, "permissions:`n contents: write`n`njobs:", 1) }
61
+ }
62
+
63
+ if ($text -match '(?m)^on:\s*(\[\s*)?workflow_dispatch' -and $text -notmatch '(?m)^\s+(push|pull_request)\s*:') {
64
+ Write-PortWarn "$n triggers only on workflow_dispatch at $SourceRef. That is what a FROZEN workflow looks like. Re-run with -SourceRef set to the last commit before it was frozen."
65
+ }
66
+
67
+ [IO.File]::WriteAllText($dest, ($text -replace "`r?`n", "`n"), (New-Object Text.UTF8Encoding($false)))
68
+ $written += ".github/workflows/$n"
69
+
70
+ $lines = $text -split "`n"
71
+ $empty = @()
72
+ for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '\$\{\{\s*\}\}') { $empty += ($i + 1) } }
73
+ if ($empty.Count -gt 0) {
74
+ $bad++
75
+ Write-PortFail "$n line(s) $($empty -join ', '): empty `${{ }}. GitHub rejects the whole file. Reword the text (a comment inside run: is still scanned)."
76
+ }
77
+ Write-PortOk "$n -> .github/workflows/$n (gitea. context rewritten: $ctx)"
78
+ $creds = @()
79
+ for ($i = 0; $i -lt $lines.Count; $i++) {
80
+ if ($lines[$i] -match 'secrets\.|github\.token|(?i)token\s*:|_TOKEN\b|_USER\b|PASSWORD') { $creds += (" {0}: {1}" -f ($i + 1), $lines[$i].Trim()) }
81
+ }
82
+ if ($creds.Count -gt 0) {
83
+ Write-Host " Credential lines - decide EACH one: ORIGIN-facing (this repo: fetch, checkout, tag push) -> `${{ github.token }} with user x-access-token; DEPENDENCY host -> a secret valid for THAT host."
84
+ $creds | ForEach-Object { Write-Host $_ }
85
+ }
86
+ }
87
+
88
+ Write-Host ''
89
+ Write-Host 'Written (review, then git add each file BY NAME):'
90
+ $written | ForEach-Object { Write-Host " $_" }
91
+ if ($bad -gt 0) { exit 1 }
92
+ exit 0