@caddis/cli 0.4.22 → 0.4.24
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/_registry.md +1 -1
- package/bundles/antigravity-plugin/skills/port-to-github/SKILL.md +64 -3
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Get-PortSource.ps1 +30 -2
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Invoke-PortHistoryClean.ps1 +123 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/New-PortRuntimeManifest.ps1 +258 -0
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Publish-PortToGitHub.ps1 +20 -1
- package/bundles/antigravity-plugin/skills/port-to-github/scripts/Test-PortContentPolicy.ps1 +218 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/SKILL.md +174 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/_template/profile.json +55 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/batch-job/profile.json +68 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/dotnet-service/profile.json +67 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/java-service/profile.json +62 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/node-service/profile.json +66 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/php-db/profile.json +75 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/python-web/profile.json +73 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/profiles/static-site/profile.json +87 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/references/config.example.json +18 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/references/profile-contract.md +207 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/references/standard.md +304 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/scripts/AdoptCommon.ps1 +812 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/scripts/Get-AppInventory.ps1 +236 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/scripts/New-AdoptionPlan.ps1 +311 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/scripts/Resolve-AppProfile.ps1 +246 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-app/scripts/Test-AppConformance.ps1 +343 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/SKILL.md +64 -3
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Get-PortSource.ps1 +30 -2
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Invoke-PortHistoryClean.ps1 +123 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/New-PortRuntimeManifest.ps1 +258 -0
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Publish-PortToGitHub.ps1 +20 -1
- package/bundles/codex/.codex/skills/devops/port-to-github/scripts/Test-PortContentPolicy.ps1 +218 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| Ci Cd Pipeline | `ci-cd-pipeline/` | CI/CD pipeline design and implementation for GitHub Actions, Azure DevOps, and general pipeline architecture. Use when creating build pipelines, deployment workflows, quality gates, environment promotion strategies, or automating release processes. |
|
|
40
40
|
| Gh Cli | `gh-cli/` | GitHub CLI operations — issues, PRs, releases, and repo management |
|
|
41
41
|
| Git Commit | `git-commit/` | Create well-structured conventional commit messages following Conventional Commits standard. Use when committing changes, preparing PRs, or generating changelogs. |
|
|
42
|
-
| Port To Github | `port-to-github/` | 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". |
|
|
42
|
+
| Port To Github | `port-to-github/` | 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, enforces a source-only content policy (runtimes, binaries, data exports and dependencies stay out of the repo, over ALL history, with a runtime manifest and provisioning script so the environment is still reproducible), 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", "keep binaries / MariaDB / PHP out of the repo", "check the repo only holds source", or "cut the deploy over to GitHub". |
|
|
43
43
|
| Safe Cleanup | `safe-cleanup/` | Use BEFORE deleting any directory tree, clearing a scratchpad, removing git worktrees, or "tidying up" generated folders - especially on Windows, where a recursive delete that meets a junction or symlink destroys the link's TARGET. Also use when verifying that a Python venv or node_modules survived a cleanup, because pip check and file counts both report healthy on a half-deleted environment. |
|
|
44
44
|
| Using Git Worktrees | `using-git-worktrees/` | Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification |
|
|
45
45
|
| Windows Deployment | `windows-deployment/` | Deploy FastAPI + React/Vite apps to Windows Server with NSSM services, reverse proxy (IIS or nginx), and git-pull workflow. Use when deploying any web app to a Windows prod server, setting up NSSM services, configuring IIS or nginx reverse proxy, making code environment-aware for dev/prod, or troubleshooting prod deployment issues. |
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
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".
|
|
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, enforces a source-only content policy (runtimes, binaries, data exports and dependencies stay out of the repo, over ALL history, with a runtime manifest and provisioning script so the environment is still reproducible), 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", "keep binaries / MariaDB / PHP out of the repo", "check the repo only holds source", or "cut the deploy over to GitHub".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# port-to-github
|
|
7
7
|
|
|
8
8
|
Move one app to GitHub without losing history, leaking a secret, or breaking its deploy.
|
|
9
9
|
|
|
10
|
-
The work is in **six phases
|
|
10
|
+
The work is in **six phases**, plus the content policy (2b). Each ends in a check that can FAIL. Do not start a phase until
|
|
11
11
|
the one before it passed. Scripts are in `scripts/` next to this file. They are PowerShell 7
|
|
12
12
|
(`pwsh`); the parts that run on a remote box are Windows PowerShell 5.1 compatible.
|
|
13
13
|
|
|
@@ -15,6 +15,7 @@ the one before it passed. Scripts are in `scripts/` next to this file. They are
|
|
|
15
15
|
|---|---|---|
|
|
16
16
|
| 1. Stage the source | `Get-PortSource.ps1` | No. The source is only read. |
|
|
17
17
|
| 2. Readiness | `Test-PortReadiness.ps1` | No. |
|
|
18
|
+
| 2b. Content policy | `Test-PortContentPolicy.ps1`, `New-PortRuntimeManifest.ps1`, `Invoke-PortHistoryClean.ps1` | No. Rewrites only the local stage. |
|
|
18
19
|
| 3. Publish | `Publish-PortToGitHub.ps1` | **Yes: creates a GitHub repo and pushes.** |
|
|
19
20
|
| 4. CI | `Convert-PortWorkflow.ps1`, `Test-PortWorkflowParses.ps1` | Yes: a feature branch and a PR. |
|
|
20
21
|
| 5. Cutover | `Switch-CheckoutOrigin.ps1` | **Yes: changes where a deploy host pulls from.** |
|
|
@@ -64,6 +65,9 @@ Read the output. These need a decision, not a shrug:
|
|
|
64
65
|
- **Uncommitted changes** in a source checkout are left out unless you pass
|
|
65
66
|
`-IncludeUncommitted`, which puts them on a separate branch `port/uncommitted-snapshot-*`.
|
|
66
67
|
The real branches stay exactly as committed. Ask the user which they want.
|
|
68
|
+
- **Default branch.** From a server or a bare repository, its HEAD. From a WORKING checkout,
|
|
69
|
+
`main` (or `master`) when it exists, because the checked-out branch is only what someone had
|
|
70
|
+
open; the script says when it made that choice. Override with `-DefaultBranch <name>`.
|
|
67
71
|
- **Remote-only branches**: branches the checkout only knows as `origin/x`. If the checkout is
|
|
68
72
|
the only copy, re-run with `-PromoteRemoteBranches origin`. If a server has them, port from it.
|
|
69
73
|
- **Likely secrets** in a plain folder or a snapshot stop the script (exit 2). Nothing has left
|
|
@@ -93,6 +97,63 @@ deploy jobs included. Readiness lists those branches; publish refuses them unles
|
|
|
93
97
|
(the new repo will expose them), CI credentials, dependency hosts, runner labels, files that
|
|
94
98
|
still name the old host. The report is in `readiness.json`.
|
|
95
99
|
|
|
100
|
+
## Phase 2b - content policy: source only
|
|
101
|
+
|
|
102
|
+
**The rule.** The repository holds application code, configuration and templates, migration
|
|
103
|
+
SCRIPTS, tests, and the scripts and manifests that rebuild the environment. Runtime and server
|
|
104
|
+
distributions (MariaDB, PHP, JDK, Node, Python, Apache...), binaries and archives, database data
|
|
105
|
+
files, data exports and installed dependencies stay OUT, and are installed or provisioned
|
|
106
|
+
separately. This is the default for every port; a project narrows or widens it with a
|
|
107
|
+
`contentPolicy` block in its config (`keep` / `exclude` regexes, `dataThresholdMB`, `largeMB`).
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
& $s/Test-PortContentPolicy.ps1 -Name <repo-name>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
It checks the default branch AND every blob in history, because **`.gitignore` does not remove a
|
|
114
|
+
file that was committed before it**. Exit 1 means excluded content exists somewhere in history.
|
|
115
|
+
The output groups findings by folder; `content-policy.json` has every path.
|
|
116
|
+
|
|
117
|
+
For each group, decide with the user:
|
|
118
|
+
|
|
119
|
+
1. **It belongs** (a small fixture, a vendored library the team maintains) -> add a `keep` regex
|
|
120
|
+
to the project config and re-run.
|
|
121
|
+
2. **It does not** -> remove it from the STAGE's history. Two ways, and it is the owner's call:
|
|
122
|
+
```powershell
|
|
123
|
+
& $s/Invoke-PortHistoryClean.ps1 -Name <repo-name> -Mode Rewrite # keep history, drop the paths
|
|
124
|
+
& $s/Invoke-PortHistoryClean.ps1 -Name <repo-name> -Mode FreshStart # one commit of today's files
|
|
125
|
+
```
|
|
126
|
+
`Rewrite` keeps every branch, tag and commit but CHANGES commit SHAs from the first affected
|
|
127
|
+
commit on; anything quoting an old SHA stops matching. `FreshStart` is smallest and drops all
|
|
128
|
+
history, other branches and tags. Both re-run the policy check and exit 1 if anything
|
|
129
|
+
excluded survived. The source keeps its original history either way.
|
|
130
|
+
|
|
131
|
+
**Keep the environment reproducible.** Excluding a runtime is only safe if the repo says how to
|
|
132
|
+
get it back. Scan the SOURCE FOLDER (runtimes usually live in an ignored `tools/`):
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
& $s/New-PortRuntimeManifest.ps1 -Path <source-folder> [-ComputerName <box>] -OutDir <clone>\environment
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It writes `runtimes.json` (kind, exact version, binary SHA-256, data folders), `provision.ps1`
|
|
139
|
+
(downloads and unpacks each runtime with a known public source and FAILS for any it cannot
|
|
140
|
+
provision), `README.md`, and `config-templates/`: configuration found inside runtime folders
|
|
141
|
+
(`my.ini`, `php.ini`, `config/`, `metadata/`). Configuration is app content even when the runtime
|
|
142
|
+
around it is not. Files that look like they hold a credential are withheld and listed; make
|
|
143
|
+
those templates by hand with placeholders. **The credential check is a heuristic: read every
|
|
144
|
+
copied template yourself before committing it.** The dogfood found one it missed (a SAML test
|
|
145
|
+
user written as `'name:password' => array(...)`); the rule now covers that form, but the next
|
|
146
|
+
form may be new. Nothing it finds is executed; versions come from file
|
|
147
|
+
metadata. Commit `environment/` on the phase 4 branch, by file name.
|
|
148
|
+
|
|
149
|
+
Two things it cannot decide, so you must:
|
|
150
|
+
- **UNCLASSIFIED folders** beside a runtime (a deployed web tree, a library with no binary).
|
|
151
|
+
Decide each: provision it, rebuild it from repo code, or keep it in the repo.
|
|
152
|
+
- **Data folders** (e.g. a database `data/` directory). This is state, not code. The repo carries
|
|
153
|
+
the schema and migrations; the data comes from a backup. Say where that backup lives.
|
|
154
|
+
|
|
155
|
+
Then add every excluded folder to `.gitignore` on the phase 4 branch, so it cannot come back.
|
|
156
|
+
|
|
96
157
|
## Phase 3 - publish (ask first)
|
|
97
158
|
|
|
98
159
|
Tell the user: the repo name, owner, visibility, branch and tag counts. Then:
|
|
@@ -101,7 +162,7 @@ Tell the user: the repo name, owner, visibility, branch and tag counts. Then:
|
|
|
101
162
|
& $s/Publish-PortToGitHub.ps1 -Name <repo-name> [-Owner <org>] [-Visibility private]
|
|
102
163
|
```
|
|
103
164
|
|
|
104
|
-
It
|
|
165
|
+
It refuses a stage whose content policy is not compliant (run phase 2b; `-AcceptContentPolicy` records an explicit owner exception instead). It then checks the stage still holds exactly the refs phase 1 recorded (anything else would be
|
|
105
166
|
pushed and then "verified" against itself), creates the repo EMPTY, pushes `refs/heads/*` and
|
|
106
167
|
`refs/tags/*` with an explicit refspec, sets the default branch, and compares every ref by SHA
|
|
107
168
|
(exit 1 on any difference). Do not run other git commands inside the stage; if you did,
|
|
@@ -37,6 +37,9 @@ param(
|
|
|
37
37
|
# What to do when a plain folder or an uncommitted snapshot contains a likely secret.
|
|
38
38
|
[ValidateSet('Stop', 'Exclude')][string]$OnSecret = 'Stop',
|
|
39
39
|
[string]$InitialBranch = 'main',
|
|
40
|
+
# The GitHub default branch. Default: the server's HEAD; for a working checkout, main/master
|
|
41
|
+
# when present (the checked-out branch is only what someone had open).
|
|
42
|
+
[string]$DefaultBranch,
|
|
40
43
|
# Replace an existing staging folder for this Name.
|
|
41
44
|
[switch]$Force
|
|
42
45
|
)
|
|
@@ -315,6 +318,7 @@ switch ($kind) {
|
|
|
315
318
|
}
|
|
316
319
|
}
|
|
317
320
|
$r.isRepo = $isRepo
|
|
321
|
+
$r.isBare = $isBare
|
|
318
322
|
if ($isRepo) {
|
|
319
323
|
$bundle = Join-Path $tmp 'repo.bundle'
|
|
320
324
|
& $git -c safe.directory=* -C $Path bundle create $bundle --all 2>&1 | Out-Null
|
|
@@ -362,7 +366,13 @@ switch ($kind) {
|
|
|
362
366
|
Invoke-PortGit @('bundle', 'verify', $localBundle) | Out-Null
|
|
363
367
|
Invoke-PortGit @('clone', '-q', '--mirror', $localBundle, $mirror) | Out-Null
|
|
364
368
|
$manifest.hadGitHistory = $true
|
|
365
|
-
|
|
369
|
+
# A bare repository's HEAD is authoritative, like a server's; only a WORKING checkout's
|
|
370
|
+
# HEAD is 'whatever was open'. Leaving sourceHead empty keeps the main-line preference off.
|
|
371
|
+
$manifest.sourceHead = if ($remote.isBare) { $null } elseif ($remote.head) { $remote.head } else { $remote.headSha }
|
|
372
|
+
# A bundle records HEAD as a commit, not a branch name, so a clone of it GUESSES the
|
|
373
|
+
# default when two branches share that commit. For a bare repo the name read on the
|
|
374
|
+
# far side is authoritative; carry it across.
|
|
375
|
+
if ($remote.isBare -and $remote.head) { $authHead = $remote.head }
|
|
366
376
|
$manifest.dirtyFiles = @($remote.dirty)
|
|
367
377
|
if ($manifest.dirtyFiles.Count -gt 0) {
|
|
368
378
|
if ($IncludeUncommitted -and $remote.dirtyZip) {
|
|
@@ -430,11 +440,29 @@ if ($heads.Count -eq 0) { throw "The source produced no branches. Nothing to por
|
|
|
430
440
|
|
|
431
441
|
$symHead = Invoke-PortGit -AllowFailure @('-C', $mirror, 'symbolic-ref', '--short', 'HEAD')
|
|
432
442
|
$def = if ($symHead.ExitCode -eq 0) { $symHead.Output -join '' } else { $null }
|
|
443
|
+
$mainLine = @('main', 'master') | Where-Object { $headNames -contains $_ } | Select-Object -First 1
|
|
444
|
+
if (-not (Get-Variable -Name authHead -ErrorAction SilentlyContinue)) { $authHead = $null }
|
|
445
|
+
# The branch the source checkout was really on (a bundle's HEAD is only a guess), so the
|
|
446
|
+
# main-line choice below can say what it replaced.
|
|
447
|
+
if ($manifest.sourceHead -and ($headNames -contains $manifest.sourceHead)) { $def = $manifest.sourceHead }
|
|
448
|
+
if ($DefaultBranch) {
|
|
449
|
+
if (-not ($headNames -contains $DefaultBranch)) { throw "-DefaultBranch '$DefaultBranch' is not a branch in the source." }
|
|
450
|
+
$def = $DefaultBranch
|
|
451
|
+
}
|
|
452
|
+
elseif ($authHead -and ($headNames -contains $authHead)) {
|
|
453
|
+
$def = $authHead
|
|
454
|
+
}
|
|
455
|
+
elseif ($kind -ne 'url' -and $manifest.sourceHead -and $mainLine -and $def -ne $mainLine) {
|
|
456
|
+
# A server's HEAD IS its default branch. A working checkout's HEAD is only whatever branch
|
|
457
|
+
# someone had open, so prefer the main line when there is one, and say so.
|
|
458
|
+
$manifest.warnings += "The checkout was on '$def', but '$mainLine' exists, so '$mainLine' is the default branch on GitHub. Pass -DefaultBranch to choose another."
|
|
459
|
+
$def = $mainLine
|
|
460
|
+
}
|
|
433
461
|
if (-not $def -or -not ($headNames -contains $def)) {
|
|
434
462
|
$def = @('main', 'master', 'develop') | Where-Object { $headNames -contains $_ } | Select-Object -First 1
|
|
435
463
|
if (-not $def) { $def = $headNames[0] }
|
|
436
|
-
Invoke-PortGit @('-C', $mirror, 'symbolic-ref', 'HEAD', "refs/heads/$def") | Out-Null
|
|
437
464
|
}
|
|
465
|
+
Invoke-PortGit @('-C', $mirror, 'symbolic-ref', 'HEAD', "refs/heads/$def") | Out-Null
|
|
438
466
|
$manifest.defaultBranch = $def
|
|
439
467
|
|
|
440
468
|
if ($manifest.remoteOnlyBranches.Count -gt 0) {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Remove excluded paths from the STAGED repository's history, then prove they are gone.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Works only on <staging>\<Name>\mirror.git. The source is never touched.
|
|
7
|
+
|
|
8
|
+
-Mode Rewrite keep every branch, tag and commit, minus the excluded paths. Commit SHAs
|
|
9
|
+
change from the first commit that touched an excluded path onward, so
|
|
10
|
+
anything that quotes an old SHA (a deploy record, a tag note) stops matching.
|
|
11
|
+
Uses git filter-repo if installed, otherwise git filter-branch.
|
|
12
|
+
-Mode FreshStart one new commit holding the default branch's current files minus the excluded
|
|
13
|
+
paths. All other branches, tags and history are dropped. Smallest, simplest,
|
|
14
|
+
and loses the history - an owner decision.
|
|
15
|
+
|
|
16
|
+
Paths come from content-policy-exclude.txt (written by Test-PortContentPolicy.ps1) unless
|
|
17
|
+
-PathsFile is given. Afterwards it re-runs Test-PortContentPolicy.ps1, which must exit 0, and
|
|
18
|
+
rewrites manifest.json's heads and tags so Publish's stage-drift check sees the cleaned refs.
|
|
19
|
+
#>
|
|
20
|
+
[CmdletBinding(SupportsShouldProcess)]
|
|
21
|
+
param(
|
|
22
|
+
[Parameter(Mandatory)][string]$Name,
|
|
23
|
+
[Parameter(Mandatory)][ValidateSet('Rewrite', 'FreshStart')][string]$Mode,
|
|
24
|
+
[string]$PathsFile,
|
|
25
|
+
[string]$StagingRoot,
|
|
26
|
+
[string]$ConfigPath
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
$ErrorActionPreference = 'Stop'
|
|
30
|
+
. (Join-Path $PSScriptRoot 'PortCommon.ps1')
|
|
31
|
+
|
|
32
|
+
$config = Get-PortConfig $ConfigPath
|
|
33
|
+
$stage = Join-Path (Get-PortStagingRoot $config $StagingRoot) $Name
|
|
34
|
+
$mirror = Join-Path $stage 'mirror.git'
|
|
35
|
+
$manifestPath = Join-Path $stage 'manifest.json'
|
|
36
|
+
if (-not (Test-Path -LiteralPath $mirror)) { throw "No staged mirror at $mirror." }
|
|
37
|
+
if (-not $PathsFile) { $PathsFile = Join-Path $stage 'content-policy-exclude.txt' }
|
|
38
|
+
if (-not (Test-Path -LiteralPath $PathsFile)) { throw "No paths file at $PathsFile. Run Test-PortContentPolicy.ps1 first." }
|
|
39
|
+
$paths = @(Get-Content -LiteralPath $PathsFile | Where-Object { $_ -and $_.Trim() })
|
|
40
|
+
if ($paths.Count -eq 0) { Write-PortOk 'Nothing to remove.'; exit 0 }
|
|
41
|
+
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
42
|
+
$branch = $manifest.defaultBranch
|
|
43
|
+
|
|
44
|
+
# git reads pathspecs as patterns; ":(literal)" makes every entry an exact path, so a file
|
|
45
|
+
# named with [ ] * ? is never widened into a glob that removes something else.
|
|
46
|
+
$specFile = Join-Path $stage 'history-clean-pathspec.txt'
|
|
47
|
+
[IO.File]::WriteAllLines($specFile, [string[]]@($paths | ForEach-Object { ":(literal)$_" }), (New-Object Text.UTF8Encoding($false)))
|
|
48
|
+
|
|
49
|
+
$before = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname) %(objectname)', 'refs/heads', 'refs/tags'))
|
|
50
|
+
if (-not $PSCmdlet.ShouldProcess($mirror, "$Mode history, removing $($paths.Count) path(s)")) { exit 0 }
|
|
51
|
+
|
|
52
|
+
if ($Mode -eq 'Rewrite') {
|
|
53
|
+
$fr = Invoke-PortGit -AllowFailure @('filter-repo', '--version')
|
|
54
|
+
if ($fr.ExitCode -eq 0) {
|
|
55
|
+
Write-PortStep "Rewriting with git filter-repo ($($paths.Count) path(s))"
|
|
56
|
+
$plain = Join-Path $stage 'history-clean-paths.txt'
|
|
57
|
+
[IO.File]::WriteAllLines($plain, [string[]]$paths, (New-Object Text.UTF8Encoding($false)))
|
|
58
|
+
Invoke-PortGit @('-C', $mirror, 'filter-repo', '--force', '--invert-paths', '--paths-from-file', $plain) | Out-Null
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
Write-PortStep "Rewriting with git filter-branch ($($paths.Count) path(s)); git filter-repo is not installed"
|
|
62
|
+
$env:FILTER_BRANCH_SQUELCH_WARNING = '1'
|
|
63
|
+
# The index filter is eval'd by a shell. Pass the path through an environment variable, so a
|
|
64
|
+
# quote in it (C:\Users\O'Brien\...) cannot break the command.
|
|
65
|
+
$env:PORT_PATHSPEC_FILE = $specFile.Replace('\', '/')
|
|
66
|
+
try {
|
|
67
|
+
$idx = 'git rm -r --cached --ignore-unmatch --quiet --pathspec-from-file="$PORT_PATHSPEC_FILE"'
|
|
68
|
+
Invoke-PortGit @('-C', $mirror, 'filter-branch', '-f', '--index-filter', $idx, '--prune-empty', '--tag-name-filter', 'cat', '--', '--all') | Out-Null
|
|
69
|
+
}
|
|
70
|
+
finally { Remove-Item Env:FILTER_BRANCH_SQUELCH_WARNING, Env:PORT_PATHSPEC_FILE -ErrorAction SilentlyContinue }
|
|
71
|
+
foreach ($r in @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname)', 'refs/original'))) {
|
|
72
|
+
if ($r) { Invoke-PortGit @('-C', $mirror, 'update-ref', '-d', $r) | Out-Null }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
Write-PortStep "Fresh start: one commit from $branch minus $($paths.Count) path(s)"
|
|
78
|
+
$tmpIndex = Join-Path $stage 'fresh.index'
|
|
79
|
+
# git rm needs a work tree even with --cached; a bare mirror has none, so lend it an empty one.
|
|
80
|
+
$tmpWork = Join-Path $stage 'fresh-worktree'
|
|
81
|
+
New-Item -ItemType Directory -Force -Path $tmpWork | Out-Null
|
|
82
|
+
$env:GIT_INDEX_FILE = $tmpIndex
|
|
83
|
+
$env:GIT_WORK_TREE = $tmpWork
|
|
84
|
+
try {
|
|
85
|
+
Invoke-PortGit @('-C', $mirror, 'read-tree', $branch) | Out-Null
|
|
86
|
+
Invoke-PortGit @('-C', $mirror, 'rm', '-r', '--cached', '--ignore-unmatch', '--quiet', "--pathspec-from-file=$specFile") | Out-Null
|
|
87
|
+
$tree = (Invoke-PortGit @('-C', $mirror, 'write-tree')) -join ''
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
Remove-Item Env:GIT_INDEX_FILE, Env:GIT_WORK_TREE -ErrorAction SilentlyContinue
|
|
91
|
+
Remove-Item -LiteralPath $tmpIndex -ErrorAction SilentlyContinue
|
|
92
|
+
Remove-Item -LiteralPath $tmpWork -Recurse -Force -ErrorAction SilentlyContinue
|
|
93
|
+
}
|
|
94
|
+
$msg = "Initial import of $Name (fresh start: history not carried over)"
|
|
95
|
+
$body = "Source: $($manifest.source), branch $branch at $((Invoke-PortGit @('-C', $mirror, 'rev-parse', $branch)) -join ''). $($paths.Count) path(s) excluded by the content policy."
|
|
96
|
+
$commit = (Invoke-PortGit @('-C', $mirror, 'commit-tree', $tree, '-m', $msg, '-m', $body)) -join ''
|
|
97
|
+
foreach ($r in @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname)', 'refs/heads', 'refs/tags'))) {
|
|
98
|
+
if ($r) { Invoke-PortGit @('-C', $mirror, 'update-ref', '-d', $r) | Out-Null }
|
|
99
|
+
}
|
|
100
|
+
Invoke-PortGit @('-C', $mirror, 'update-ref', "refs/heads/$branch", $commit) | Out-Null
|
|
101
|
+
Invoke-PortGit @('-C', $mirror, 'symbolic-ref', 'HEAD', "refs/heads/$branch") | Out-Null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Drop the unreachable objects, so a later readiness scan measures what will actually be pushed.
|
|
105
|
+
Invoke-PortGit @('-C', $mirror, 'reflog', 'expire', '--expire=now', '--all') | Out-Null
|
|
106
|
+
Invoke-PortGit @('-C', $mirror, 'gc', '--prune=now', '--quiet') | Out-Null
|
|
107
|
+
|
|
108
|
+
$after = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname) %(objectname)', 'refs/heads', 'refs/tags'))
|
|
109
|
+
$manifest.heads = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/heads'))
|
|
110
|
+
$manifest.tags = @(Invoke-PortGit @('-C', $mirror, 'for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/tags'))
|
|
111
|
+
$manifest | Add-Member -NotePropertyName historyClean -NotePropertyValue ([pscustomobject]@{
|
|
112
|
+
mode = $Mode; at = (Get-Date).ToString('o'); pathsRemoved = $paths.Count
|
|
113
|
+
refsBefore = $before.Count; refsAfter = $after.Count
|
|
114
|
+
note = 'Commit SHAs changed. The source still holds the original history.'
|
|
115
|
+
}) -Force
|
|
116
|
+
ConvertTo-PortJsonFile $manifest $manifestPath
|
|
117
|
+
|
|
118
|
+
# The proof: the policy must now pass. A clean that cannot fail is not a clean.
|
|
119
|
+
Write-PortStep 'Re-checking the content policy on the cleaned history'
|
|
120
|
+
& (Join-Path $PSScriptRoot 'Test-PortContentPolicy.ps1') -Name $Name -StagingRoot $StagingRoot -ConfigPath $ConfigPath
|
|
121
|
+
if ($LASTEXITCODE -ne 0) { Write-PortFail 'Excluded content is STILL in history after the clean. Do not publish.'; exit 1 }
|
|
122
|
+
Write-PortOk ("History cleaned ({0}): refs {1} -> {2}. Re-run Test-PortReadiness.ps1 before publishing." -f $Mode, $before.Count, $after.Count)
|
|
123
|
+
exit 0
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Record the runtimes an app needs but the repository must not carry, so the environment can
|
|
4
|
+
be rebuilt from the repository alone.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Scans the app's SOURCE FOLDER (not the git history), including ignored folders such as tools/,
|
|
8
|
+
for runtime distributions: MariaDB/MySQL, PHP, Node, Python, Java, Apache, nginx, Redis,
|
|
9
|
+
PostgreSQL. Versions come from file metadata. NOTHING it finds is executed.
|
|
10
|
+
|
|
11
|
+
Writes to -OutDir (put it in the repository, e.g. environment/):
|
|
12
|
+
runtimes.json name, version, where it lived, binary SHA-256, data folders, config files
|
|
13
|
+
README.md the same for people, plus every decision still open
|
|
14
|
+
provision.ps1 downloads and unpacks each runtime with a known public source; fails
|
|
15
|
+
loudly for any runtime it cannot provision, so a gap is never silent
|
|
16
|
+
config-templates/ configuration found INSIDE runtime folders (my.ini, php.ini, config/,
|
|
17
|
+
metadata/). The teammate rule keeps configuration in the repo even when
|
|
18
|
+
the runtime around it is excluded. A file that looks like it holds a
|
|
19
|
+
credential is NOT copied; it is listed for a hand-made template.
|
|
20
|
+
|
|
21
|
+
Folders beside a runtime that match nothing (a deployed web tree, a vendored library) are
|
|
22
|
+
listed as UNCLASSIFIED: each needs a human decision.
|
|
23
|
+
|
|
24
|
+
Works on a local or UNC path, or on a remote box with -ComputerName (Windows PowerShell 5.1
|
|
25
|
+
on the far side is fine).
|
|
26
|
+
#>
|
|
27
|
+
[CmdletBinding()]
|
|
28
|
+
param(
|
|
29
|
+
[Parameter(Mandatory)][string]$Path,
|
|
30
|
+
[string]$ComputerName,
|
|
31
|
+
[pscredential]$Credential,
|
|
32
|
+
[Parameter(Mandatory)][string]$OutDir,
|
|
33
|
+
[int]$MaxDepth = 5,
|
|
34
|
+
[int]$ConfigMaxKB = 256
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
$ErrorActionPreference = 'Stop'
|
|
38
|
+
. (Join-Path $PSScriptRoot 'PortCommon.ps1')
|
|
39
|
+
|
|
40
|
+
# Runs locally or remotely. 5.1-compatible, read-only, never executes a found binary.
|
|
41
|
+
$scan = {
|
|
42
|
+
param($Root, $Depth, $CfgMaxKB)
|
|
43
|
+
$ErrorActionPreference = 'Continue'
|
|
44
|
+
$res = @{ ok = $false; error = $null; runtimes = @(); unclassified = @() }
|
|
45
|
+
if (-not (Test-Path -LiteralPath $Root -PathType Container)) { $res.error = "Folder not found: $Root"; return $res }
|
|
46
|
+
$Root = (Resolve-Path -LiteralPath $Root).ProviderPath.TrimEnd('\')
|
|
47
|
+
function Rel($p) { $p.Substring($Root.Length).TrimStart('\').Replace('\', '/') }
|
|
48
|
+
# A plain loop: Measure-Object over an EMPTY folder has no .Sum under strict mode, and an
|
|
49
|
+
# empty data/ folder (a runtime never started) is normal here.
|
|
50
|
+
function DirMB($p) { $t = [int64]0; foreach ($f in @(Get-ChildItem -LiteralPath $p -Recurse -File -Force -ErrorAction SilentlyContinue)) { $t += $f.Length }; [math]::Round($t / 1MB, 1) }
|
|
51
|
+
|
|
52
|
+
$binRx = '(?i)^(mariadbd|mysqld|php|node|python|java|httpd|nginx|redis-server|postgres)\.exe$'
|
|
53
|
+
$skipRx = '(?i)\\(node_modules|\.venv|venv|\.git|site-packages|vendor)\\'
|
|
54
|
+
$bins = @(Get-ChildItem -LiteralPath $Root -Recurse -Depth $Depth -File -Force -ErrorAction SilentlyContinue |
|
|
55
|
+
Where-Object { $_.Name -match $binRx -and $_.FullName -notmatch $skipRx })
|
|
56
|
+
$roots = @{}
|
|
57
|
+
$priority = @('mariadbd', 'mysqld', 'php', 'node', 'python', 'java', 'httpd', 'nginx', 'redis-server', 'postgres')
|
|
58
|
+
foreach ($b in $bins) {
|
|
59
|
+
$dir = $b.Directory
|
|
60
|
+
$rootDir = if ($dir.Name -ieq 'bin' -and $dir.Parent) { $dir.Parent.FullName } else { $dir.FullName }
|
|
61
|
+
if ($rootDir -ieq $Root) { continue }
|
|
62
|
+
$base = [IO.Path]::GetFileNameWithoutExtension($b.Name).ToLower()
|
|
63
|
+
if (-not $roots.ContainsKey($rootDir) -or ($priority.IndexOf($base) -lt $priority.IndexOf($roots[$rootDir].base))) {
|
|
64
|
+
$roots[$rootDir] = @{ file = $b; base = $base }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
# A runtime nested inside another runtime (php inside apache/) belongs to the outer one.
|
|
68
|
+
$rootList = @($roots.Keys | Sort-Object Length)
|
|
69
|
+
$kept = @()
|
|
70
|
+
foreach ($r in $rootList) { if (-not ($kept | Where-Object { $r.StartsWith($_ + '\', [StringComparison]::OrdinalIgnoreCase) })) { $kept += $r } }
|
|
71
|
+
|
|
72
|
+
$cfgExt = '(?i)\.(ini|cnf|conf|php|ya?ml|json|xml|properties|toml)$'
|
|
73
|
+
function Get-Configs($dir, $dataDirs) {
|
|
74
|
+
$out = @()
|
|
75
|
+
$cands = @(Get-ChildItem -LiteralPath $dir -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)\.(ini|cnf|conf)$' })
|
|
76
|
+
foreach ($sub in @(Get-ChildItem -LiteralPath $dir -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)^(conf|config|etc|metadata|cert)$' })) {
|
|
77
|
+
$cands += @(Get-ChildItem -LiteralPath $sub.FullName -Recurse -Depth 2 -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match $cfgExt -or $sub.Name -ieq 'cert' })
|
|
78
|
+
}
|
|
79
|
+
foreach ($f in $cands) {
|
|
80
|
+
if ($dataDirs | Where-Object { $f.FullName.StartsWith($_ + '\', [StringComparison]::OrdinalIgnoreCase) }) { continue }
|
|
81
|
+
if ($f.Name -match '(?i)\.(dist|example|sample|template)$') { continue }
|
|
82
|
+
$o = @{ rel = (Rel $f.FullName); bytes = $f.Length; b64 = $null; tooBig = $false }
|
|
83
|
+
if ($f.Length -le ($CfgMaxKB * 1KB)) { $o.b64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($f.FullName)) } else { $o.tooBig = $true }
|
|
84
|
+
$out += $o
|
|
85
|
+
}
|
|
86
|
+
return , $out
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
foreach ($rd in $kept) {
|
|
90
|
+
$e = $roots[$rd]; $vi = $e.file.VersionInfo
|
|
91
|
+
$ver = "$($vi.ProductVersion)".Trim(); if (-not $ver) { $ver = "$($vi.FileVersion)".Trim() }
|
|
92
|
+
$kind = switch -Regex ($e.base) { '^(mariadbd|mysqld)$' { if ("$($vi.ProductName)$($vi.FileDescription)" -match '(?i)mariadb' -or (Test-Path (Join-Path $rd 'bin\mariadbd.exe'))) { 'mariadb' } else { 'mysql' } } '^php$' { 'php' } '^node$' { 'node' } '^python$' { 'python' } '^java$' { 'java' } '^httpd$' { 'apache' } '^nginx$' { 'nginx' } '^redis-server$' { 'redis' } '^postgres$' { 'postgresql' } default { $e.base } }
|
|
93
|
+
$sha = (Get-FileHash -LiteralPath $e.file.FullName -Algorithm SHA256).Hash.ToLower()
|
|
94
|
+
$dataDirs = @(Get-ChildItem -LiteralPath $rd -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '(?i)^(data|db|pgdata|var)$' } | ForEach-Object { $_.FullName })
|
|
95
|
+
$ts = $null
|
|
96
|
+
if ($kind -eq 'php') { $ts = [bool](Get-ChildItem -LiteralPath $rd -Filter 'php*ts.dll' -File -ErrorAction SilentlyContinue) }
|
|
97
|
+
$res.runtimes += @{
|
|
98
|
+
kind = $kind; root = (Rel $rd); binary = (Rel $e.file.FullName); version = $ver
|
|
99
|
+
product = "$($vi.ProductName)"; binarySha256 = $sha; sizeMB = (DirMB $rd); threadSafe = $ts
|
|
100
|
+
dataDirs = @($dataDirs | ForEach-Object { @{ rel = (Rel $_); sizeMB = (DirMB $_) } })
|
|
101
|
+
configs = (Get-Configs $rd $dataDirs)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Siblings of runtime roots that match nothing: a human must decide what they are.
|
|
106
|
+
$containers = @($kept | ForEach-Object { Split-Path $_ -Parent } | Sort-Object -Unique | Where-Object { $_ -and ($_ -ine $Root) })
|
|
107
|
+
foreach ($c in $containers) {
|
|
108
|
+
foreach ($d in @(Get-ChildItem -LiteralPath $c -Directory -Force -ErrorAction SilentlyContinue)) {
|
|
109
|
+
if ($kept -contains $d.FullName) { continue }
|
|
110
|
+
$hints = @()
|
|
111
|
+
$cj = Join-Path $d.FullName 'composer.json'
|
|
112
|
+
if (Test-Path $cj) { try { $j = Get-Content $cj -Raw | ConvertFrom-Json; $hints += "composer package $($j.name)$(if ($j.version) { ' ' + $j.version })" } catch { $hints += 'composer.json' } }
|
|
113
|
+
if (Test-Path (Join-Path $d.FullName 'package.json')) { $hints += 'package.json' }
|
|
114
|
+
foreach ($s in 'conf', 'config', 'cert', 'data', 'log') { if (Test-Path (Join-Path $d.FullName $s)) { $hints += "$s/" } }
|
|
115
|
+
$res.unclassified += @{ rel = (Rel $d.FullName); sizeMB = (DirMB $d.FullName); hints = $hints; configs = (Get-Configs $d.FullName @()) }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
$res.ok = $true
|
|
119
|
+
return $res
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ($ComputerName) {
|
|
123
|
+
$ia = @{ ComputerName = $ComputerName; ScriptBlock = $scan; ArgumentList = @($Path, $MaxDepth, $ConfigMaxKB) }
|
|
124
|
+
if ($Credential) { $ia.Credential = $Credential }
|
|
125
|
+
Write-PortStep "Scanning $Path on $ComputerName for runtimes (read-only)"
|
|
126
|
+
$res = Invoke-Command @ia
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
Write-PortStep "Scanning $Path for runtimes (read-only)"
|
|
130
|
+
$res = & $scan $Path $MaxDepth $ConfigMaxKB
|
|
131
|
+
}
|
|
132
|
+
if (-not $res.ok) { throw $res.error }
|
|
133
|
+
|
|
134
|
+
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
|
135
|
+
$tplRoot = Join-Path $OutDir 'config-templates'
|
|
136
|
+
$withheld = @(); $copied = @(); $tooBig = @()
|
|
137
|
+
function Save-Configs($list) {
|
|
138
|
+
foreach ($c in @($list)) {
|
|
139
|
+
if ($c.tooBig) { $script:tooBig += $c.rel; continue }
|
|
140
|
+
$bytes = [Convert]::FromBase64String($c.b64)
|
|
141
|
+
$text = [Text.Encoding]::UTF8.GetString($bytes)
|
|
142
|
+
$leaf = Split-Path $c.rel -Leaf
|
|
143
|
+
# Heuristics, so they err toward WITHHOLDING. Found by the dogfood: SimpleSAMLphp's
|
|
144
|
+
# exampleauth writes a user as 'name:password' => array(...), which no key=value rule sees.
|
|
145
|
+
$secret = (Test-PortSecretName $leaf) -or ($c.rel -match '(?i)(^|/)cert/') -or (Test-PortSecretContent $text) -or
|
|
146
|
+
($text -match '(?i)(password|passwd|pass|pwd|secret|salt|token|api_?key|private_?key|auth\.adminpassword|credentials?)\s*[''"]?\s*(=>|=|:)\s*[''"][^''"\s]{4,}[''"]') -or
|
|
147
|
+
($text -match '[''"][^''"\s:/]{2,}:[^''"\s]{4,}[''"]\s*=>') -or
|
|
148
|
+
($text -match '(?i)[a-z][a-z0-9+.-]*://[^/\s:@''"]+:[^/\s@''"]+@') -or
|
|
149
|
+
# Unquoted INI/properties form, the one my.ini and php.ini actually use: password=Secret123
|
|
150
|
+
($text -match '(?im)^\s*[\w.-]*(password|passwd|pwd|pass|_pw|secret|api[_-]?key|token|credential)s?\s*=\s*[^\s;#]+')
|
|
151
|
+
if ($secret) { $script:withheld += $c.rel; continue }
|
|
152
|
+
$dst = Join-Path $tplRoot ($c.rel -replace '/', '\')
|
|
153
|
+
New-Item -ItemType Directory -Force -Path (Split-Path $dst -Parent) | Out-Null
|
|
154
|
+
[IO.File]::WriteAllBytes($dst, $bytes)
|
|
155
|
+
$script:copied += $c.rel
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
foreach ($r in $res.runtimes) { Save-Configs $r.configs }
|
|
159
|
+
foreach ($u in $res.unclassified) { Save-Configs $u.configs }
|
|
160
|
+
|
|
161
|
+
function Get-Source($r) {
|
|
162
|
+
$v = ($r.version -split '\.')
|
|
163
|
+
$v3 = ($v | Select-Object -First 3) -join '.'
|
|
164
|
+
switch ($r.kind) {
|
|
165
|
+
'mariadb' { return @("https://archive.mariadb.org/mariadb-$v3/winx64-packages/mariadb-$v3-winx64.zip") }
|
|
166
|
+
'php' {
|
|
167
|
+
$mm = [version](($v | Select-Object -First 2) -join '.')
|
|
168
|
+
$vs = if ($mm -ge [version]'8.4') { 'vs17' } elseif ($mm -ge [version]'8.0') { 'vs16' } else { 'vc15' }
|
|
169
|
+
$ts = if ($r.threadSafe) { 'Win32' } else { 'nts-Win32' }
|
|
170
|
+
$f = "php-$v3-$ts-$vs-x64.zip"
|
|
171
|
+
return @("https://windows.php.net/downloads/releases/$f", "https://windows.php.net/downloads/releases/archives/$f")
|
|
172
|
+
}
|
|
173
|
+
'node' { return @("https://nodejs.org/dist/v$v3/node-v$v3-win-x64.zip") }
|
|
174
|
+
'python' { return @("https://www.python.org/ftp/python/$v3/python-$v3-embed-amd64.zip") }
|
|
175
|
+
default { return @() }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
$runtimes = @(foreach ($r in $res.runtimes) {
|
|
180
|
+
[ordered]@{
|
|
181
|
+
kind = $r.kind; version = $r.version; product = $r.product; wasAt = $r.root; binary = $r.binary
|
|
182
|
+
binarySha256 = $r.binarySha256; installedSizeMB = $r.sizeMB; threadSafe = $r.threadSafe
|
|
183
|
+
sources = @(Get-Source $r); archiveSha256 = $null
|
|
184
|
+
dataDirs = @($r.dataDirs | ForEach-Object { [ordered]@{ path = $_.rel; sizeMB = $_.sizeMB } })
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
$uncl = @(foreach ($u in $res.unclassified) { [ordered]@{ path = $u.rel; sizeMB = $u.sizeMB; hints = @($u.hints); decision = 'OPEN' } })
|
|
188
|
+
$doc = [ordered]@{
|
|
189
|
+
generatedAt = (Get-Date).ToString('o'); scannedPath = $Path; computerName = $ComputerName
|
|
190
|
+
runtimes = $runtimes; unclassified = $uncl
|
|
191
|
+
configTemplates = @($copied); configWithheld = @($withheld); configTooBig = @($tooBig)
|
|
192
|
+
}
|
|
193
|
+
ConvertTo-PortJsonFile $doc (Join-Path $OutDir 'runtimes.json')
|
|
194
|
+
|
|
195
|
+
# ---- provision.ps1: downloads what has a known source, and FAILS for the rest ----------------
|
|
196
|
+
$prov = New-Object System.Collections.Generic.List[string]
|
|
197
|
+
$prov.Add('# Generated by port-to-github New-PortRuntimeManifest.ps1. Rebuilds the local runtimes that the')
|
|
198
|
+
$prov.Add('# repository deliberately does not carry. Review the URLs once, then record each archive''s')
|
|
199
|
+
$prov.Add('# SHA-256 in runtimes.json (archiveSha256) so later runs verify the download.')
|
|
200
|
+
$prov.Add('[CmdletBinding()] param([string]$ToolsRoot = (Join-Path $PSScriptRoot ''..\tools''), [switch]$Force)')
|
|
201
|
+
$prov.Add('$ErrorActionPreference = ''Stop''')
|
|
202
|
+
$prov.Add('$spec = Get-Content -LiteralPath (Join-Path $PSScriptRoot ''runtimes.json'') -Raw | ConvertFrom-Json')
|
|
203
|
+
$prov.Add('$missing = @()')
|
|
204
|
+
$prov.Add('New-Item -ItemType Directory -Force -Path $ToolsRoot | Out-Null')
|
|
205
|
+
$prov.Add('foreach ($r in $spec.runtimes) {')
|
|
206
|
+
$prov.Add(' $dest = Join-Path $ToolsRoot (Split-Path $r.wasAt -Leaf)')
|
|
207
|
+
$prov.Add(' if ((Test-Path $dest) -and -not $Force) { Write-Host "[skip] $($r.kind) $($r.version): $dest exists"; continue }')
|
|
208
|
+
$prov.Add(' $replace = Test-Path $dest')
|
|
209
|
+
$prov.Add(' if (@($r.sources).Count -eq 0) { $missing += "$($r.kind) $($r.version) (no known public source)"; continue }')
|
|
210
|
+
$prov.Add(' $zip = Join-Path ([IO.Path]::GetTempPath()) "$($r.kind)-$($r.version).zip"; $got = $false')
|
|
211
|
+
$prov.Add(' foreach ($u in $r.sources) { try { Invoke-WebRequest -Uri $u -OutFile $zip -UseBasicParsing; $got = $true; break } catch { Write-Host "[miss] $u" } }')
|
|
212
|
+
$prov.Add(' if (-not $got) { $missing += "$($r.kind) $($r.version) (download failed)"; continue }')
|
|
213
|
+
$prov.Add(' $h = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()')
|
|
214
|
+
$prov.Add(' if ($r.archiveSha256 -and $h -ne $r.archiveSha256) { throw "$($r.kind): archive SHA-256 $h does not match runtimes.json $($r.archiveSha256)" }')
|
|
215
|
+
$prov.Add(' if (-not $r.archiveSha256) { Write-Warning "$($r.kind): unverified download, SHA-256 $h - record it in runtimes.json" }')
|
|
216
|
+
$prov.Add(' $tmp = Join-Path ([IO.Path]::GetTempPath()) "$($r.kind)-$($r.version)-x"; Expand-Archive -LiteralPath $zip -DestinationPath $tmp -Force')
|
|
217
|
+
$prov.Add(' $inner = @(Get-ChildItem $tmp); $src = if ($inner.Count -eq 1 -and $inner[0].PSIsContainer) { $inner[0].FullName } else { $tmp }')
|
|
218
|
+
$prov.Add(' if ($replace) { Remove-Item -LiteralPath $dest -Recurse -Force } # Move-Item -Force onto a folder NESTS; replace it instead')
|
|
219
|
+
$prov.Add(' Move-Item -LiteralPath $src -Destination $dest; Write-Host "[ok] $($r.kind) $($r.version) -> $dest"')
|
|
220
|
+
$prov.Add(' $bin = Join-Path $ToolsRoot ((Split-Path $r.wasAt -Leaf) + ''/'' + ($r.binary.Substring($r.wasAt.Length).TrimStart(''/'')))')
|
|
221
|
+
$prov.Add(' if (-not (Test-Path $bin)) { $missing += "$($r.kind): expected binary $bin not found after unpack" }')
|
|
222
|
+
$prov.Add(' elseif ((Get-FileHash $bin -Algorithm SHA256).Hash.ToLower() -ne $r.binarySha256) { Write-Warning "$($r.kind): binary differs from the recorded build (different package variant?)" }')
|
|
223
|
+
$prov.Add('}')
|
|
224
|
+
$prov.Add('Write-Host ''Copy config-templates\ into place, fill in every withheld secret, and restore data from its backup - see README.md.''')
|
|
225
|
+
$prov.Add('if ($missing.Count) { $missing | ForEach-Object { Write-Host "[FAIL] $_" }; exit 1 }')
|
|
226
|
+
$prov.Add('exit 0')
|
|
227
|
+
[IO.File]::WriteAllLines((Join-Path $OutDir 'provision.ps1'), [string[]]$prov, (New-Object Text.UTF8Encoding($false)))
|
|
228
|
+
|
|
229
|
+
# ---- README.md ------------------------------------------------------------------------------------
|
|
230
|
+
$md = New-Object System.Collections.Generic.List[string]
|
|
231
|
+
$md.Add('# Local runtime environment')
|
|
232
|
+
$md.Add('')
|
|
233
|
+
$md.Add('The runtimes below are needed to run this app locally or in UAT. They are NOT in the repository:')
|
|
234
|
+
$md.Add('they are third-party distributions, not application source. `provision.ps1` rebuilds them.')
|
|
235
|
+
$md.Add('Generated by the port-to-github skill; edit freely, but keep `runtimes.json` the source of truth.')
|
|
236
|
+
$md.Add('')
|
|
237
|
+
$md.Add('| Runtime | Version | Was at | Installed MB | Source |')
|
|
238
|
+
$md.Add('|---|---|---|---|---|')
|
|
239
|
+
foreach ($r in $runtimes) { $md.Add(('| {0} | {1} | `{2}` | {3} | {4} |' -f $r.kind, $r.version, $r.wasAt, $r.installedSizeMB, $(if (@($r.sources).Count) { $r.sources[0] } else { '**none known - install by hand**' }))) }
|
|
240
|
+
$data = @($runtimes | ForEach-Object { $k = $_.kind; $_.dataDirs | ForEach-Object { "- ``$($_.path)`` ($k, $($_.sizeMB) MB): runtime STATE, not code. Rebuild from the schema and migrations in this repository, or restore from backup. Never commit it." } })
|
|
241
|
+
if ($data.Count) { $md.Add(''); $md.Add('## Data folders (never committed)'); $data | ForEach-Object { $md.Add($_) } }
|
|
242
|
+
if ($uncl.Count) {
|
|
243
|
+
$md.Add(''); $md.Add('## OPEN: folders beside the runtimes that nothing identified')
|
|
244
|
+
$md.Add('Decide each: provision it (add it here), rebuild it from repository code, or keep it in the repository.')
|
|
245
|
+
foreach ($u in $uncl) { $md.Add(('- `{0}` ({1} MB){2}' -f $u.path, $u.sizeMB, $(if (@($u.hints).Count) { ' - ' + ($u.hints -join ', ') } else { '' }))) }
|
|
246
|
+
}
|
|
247
|
+
$md.Add(''); $md.Add('## Configuration')
|
|
248
|
+
$md.Add("- Copied to ``config-templates/`` ($($copied.Count) file(s)): configuration that lived inside a runtime folder. It IS part of the app. **Read each one before committing it**: the credential check is a heuristic.")
|
|
249
|
+
if ($withheld.Count) { $md.Add("- **Withheld, $($withheld.Count) file(s) that look like they hold a credential.** Make a template by hand with the secret replaced by a placeholder:"); $withheld | ForEach-Object { $md.Add(" - ``$_``") } }
|
|
250
|
+
if ($tooBig.Count) { $md.Add("- Too large to copy automatically ($($tooBig.Count)): " + (($tooBig | ForEach-Object { "``$_``" }) -join ', ')) }
|
|
251
|
+
[IO.File]::WriteAllLines((Join-Path $OutDir 'README.md'), [string[]]$md, (New-Object Text.UTF8Encoding($false)))
|
|
252
|
+
|
|
253
|
+
Write-Host ''
|
|
254
|
+
Write-Host ("Runtimes: {0} unclassified folders: {1} config templates: {2} withheld (credentials): {3}" -f $runtimes.Count, $uncl.Count, $copied.Count, $withheld.Count)
|
|
255
|
+
foreach ($r in $runtimes) { Write-Host (" {0,-11} {1,-12} {2,-35} {3} MB{4}" -f $r.kind, $r.version, $r.wasAt, $r.installedSizeMB, $(if (@($r.dataDirs).Count) { " data: " + (($r.dataDirs | ForEach-Object { "$($_.path) $($_.sizeMB) MB" }) -join ', ') } else { '' })) }
|
|
256
|
+
foreach ($u in $uncl) { Write-PortWarn ("UNCLASSIFIED {0} ({1} MB) {2}" -f $u.path, $u.sizeMB, ($u.hints -join ', ')) }
|
|
257
|
+
Write-Host " written to $OutDir (runtimes.json, README.md, provision.ps1, config-templates/)"
|
|
258
|
+
exit 0
|
|
@@ -27,7 +27,10 @@ param(
|
|
|
27
27
|
[switch]$AllowExisting,
|
|
28
28
|
# The staged branches already contain .github/workflows; pushing them WILL run those
|
|
29
29
|
# workflows (a deploy job included). Pass only after reading them.
|
|
30
|
-
[switch]$AllowWorkflowRuns
|
|
30
|
+
[switch]$AllowWorkflowRuns,
|
|
31
|
+
# Publish although the content policy is not met. An explicit owner exception, recorded in
|
|
32
|
+
# manifest.json; never a default.
|
|
33
|
+
[switch]$AcceptContentPolicy
|
|
31
34
|
)
|
|
32
35
|
|
|
33
36
|
$ErrorActionPreference = 'Stop'
|
|
@@ -44,6 +47,22 @@ $readinessPath = Join-Path $stage 'readiness.json'
|
|
|
44
47
|
if (-not (Test-Path -LiteralPath $readinessPath)) { throw "No readiness report. Run Test-PortReadiness.ps1 -Name $Name first." }
|
|
45
48
|
$readiness = Get-Content -LiteralPath $readinessPath -Raw | ConvertFrom-Json
|
|
46
49
|
if (-not $readiness.ready) { throw "Readiness has $(@($readiness.blockers).Count) blocker(s). Fix them and re-run Test-PortReadiness.ps1 before publishing." }
|
|
50
|
+
$policyPath = Join-Path $stage 'content-policy.json'
|
|
51
|
+
if (-not (Test-Path -LiteralPath $policyPath)) {
|
|
52
|
+
throw "No content-policy report. Run Test-PortContentPolicy.ps1 -Name $Name first (phase 2b)."
|
|
53
|
+
}
|
|
54
|
+
$policyReport = Get-Content -LiteralPath $policyPath -Raw | ConvertFrom-Json
|
|
55
|
+
# The report must describe THIS stage: a clean or re-stage after the check makes it stale.
|
|
56
|
+
if ((Get-Item -LiteralPath $policyPath).LastWriteTime -lt (Get-Item -LiteralPath $manifestPath).LastWriteTime) {
|
|
57
|
+
throw "content-policy.json is older than manifest.json, so it does not describe the current stage. Re-run Test-PortContentPolicy.ps1."
|
|
58
|
+
}
|
|
59
|
+
if (-not $policyReport.compliant) {
|
|
60
|
+
if (-not $AcceptContentPolicy) {
|
|
61
|
+
throw "Content policy not met: $($policyReport.excludedPaths) excluded path(s), $($policyReport.excludedMB) MB in history. Clean it (Invoke-PortHistoryClean.ps1), add keep rules, or pass -AcceptContentPolicy as an explicit owner exception."
|
|
62
|
+
}
|
|
63
|
+
Write-PortWarn "Publishing WITH $($policyReport.excludedPaths) excluded path(s) by explicit owner exception (-AcceptContentPolicy)."
|
|
64
|
+
$manifest | Add-Member -NotePropertyName contentPolicyException -NotePropertyValue ([pscustomobject]@{ at = (Get-Date).ToString('o'); excludedPaths = $policyReport.excludedPaths; excludedMB = $policyReport.excludedMB }) -Force
|
|
65
|
+
}
|
|
47
66
|
$live = @()
|
|
48
67
|
if ($readiness.PSObject.Properties.Name -contains 'branchesWithGithubWorkflows') { $live = @($readiness.branchesWithGithubWorkflows) }
|
|
49
68
|
if ($live.Count -gt 0 -and -not $AllowWorkflowRuns) {
|