mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-08 07:28:18 +00:00
Compare commits
25 Commits
v1.0.0.0.2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5406c85837 | |||
| 5bc77f2de1 | |||
| 57edc629c1 | |||
| 73dd27c4f1 | |||
| b26be3cd2a | |||
| 22a7387548 | |||
| 86938d0d80 | |||
| 90f43010c1 | |||
| 1c7d1d639b | |||
| c317b139d6 | |||
| d4cfa980ed | |||
| 05b771e64a | |||
| 193d6d86a1 | |||
| f2e6302d00 | |||
| 0e7b80b4ae | |||
| 732a448be5 | |||
| 573e01021b | |||
| 3b0194af93 | |||
| f27f9dc5bc | |||
| 16c56dc3af | |||
| 32e8d7430f | |||
| fc52501999 | |||
| 4ebb596176 | |||
| c968517aef | |||
| da4957dbdd |
14
.gitattributes
vendored
14
.gitattributes
vendored
@ -4,8 +4,18 @@
|
||||
*.ps1 text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as
|
||||
# part of the filename and reports every entry as missing.
|
||||
# Every .txt here is either a generated twin or a manifest, and all of them
|
||||
# want LF. plaintext_twins.py writes LF and git stores LF, but without this
|
||||
# pin checkout applied core.autocrlf and handed the working tree CRLF - so
|
||||
# every regeneration rewrote the file to LF, `git status` reported a change,
|
||||
# and `git add` normalized it straight back to nothing (#88). Pinning the
|
||||
# checkout makes all three agree.
|
||||
*.txt text eol=lf
|
||||
|
||||
# Kept explicit even though *.txt already covers it: the checksum manifest
|
||||
# must stay LF because `sha256sum -c` treats a trailing CR as part of the
|
||||
# filename and reports every entry as missing. That is a broken verification,
|
||||
# not a cosmetic diff, and it should not depend on a glob above it.
|
||||
CHECKSUMS.txt text eol=lf
|
||||
releases/*.sha256 text eol=lf
|
||||
|
||||
|
||||
51
.github/workflows/rerun-timed-out.yml
vendored
Normal file
51
.github/workflows/rerun-timed-out.yml
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
# Re-runs a test run once when one of its jobs ran out of time
|
||||
# (evo.testsuites#25, part 2).
|
||||
#
|
||||
# A timeout usually means a stuck runner or a network stall rather than a
|
||||
# broken test, so the first one gets a second chance. A second timeout stays
|
||||
# red, so a real hang still reaches a person. A run cancelled by hand, or by a
|
||||
# newer push, is left alone: only a job whose own record says it "exceeded the
|
||||
# maximum execution time" counts.
|
||||
#
|
||||
# Costs nothing on an ordinary run. The job's `if` is false for every run that
|
||||
# ended any other way, and a job skipped by its `if` never starts a runner.
|
||||
name: re-run a timed-out test run
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["tests"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
rerun:
|
||||
if: >-
|
||||
github.event.workflow_run.run_attempt == 1 &&
|
||||
(github.event.workflow_run.conclusion == 'cancelled' ||
|
||||
github.event.workflow_run.conclusion == 'timed_out')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Re-run it if a job ran out of time
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN: ${{ github.event.workflow_run.id }}
|
||||
run: |
|
||||
timed_out=""
|
||||
for job in $(gh api "repos/$REPO/actions/runs/$RUN/jobs" \
|
||||
--jq '.jobs[] | select(.conclusion == "cancelled" or .conclusion == "timed_out") | .id'); do
|
||||
if gh api "repos/$REPO/check-runs/$job/annotations" --jq '.[].message' \
|
||||
| grep -q "exceeded the maximum execution time"; then
|
||||
timed_out="$job"
|
||||
fi
|
||||
done
|
||||
if [ -n "$timed_out" ]; then
|
||||
echo "Job $timed_out ran out of time. Re-running run $RUN once."
|
||||
gh api -X POST "repos/$REPO/actions/runs/$RUN/rerun"
|
||||
else
|
||||
echo "Run $RUN was cancelled, but not by a timeout. Leaving it."
|
||||
fi
|
||||
80
.github/workflows/tests.yml
vendored
Normal file
80
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
# The Pester suite, on every push to master and every PR.
|
||||
#
|
||||
# This repo is one of the twelve on the fleet board
|
||||
# (evomedia.net/testsuites.html) and was one of three with no CI at all, so the
|
||||
# only thing ever running these tests was a workstation at 04:00. That is a
|
||||
# poor place for the only copy of a check to live.
|
||||
#
|
||||
# windows-latest, not ubuntu, even though Pester runs on Linux: these scripts
|
||||
# deploy from a Windows workstation and the suite reads like it - paths,
|
||||
# executables, the shell itself. Proving them on Linux would be proving
|
||||
# something nobody runs. Windows minutes bill at double, which this suite's
|
||||
# size affords.
|
||||
name: tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
|
||||
# Read-only: this job builds nothing and publishes nothing, so the default
|
||||
# write-capable token is more than it needs.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: tests-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
# Actions pinned to a commit, not a moving tag: a tag can be repointed
|
||||
# by whoever owns it, and this token, read-only though it is, still sees
|
||||
# the repository.
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
|
||||
# Pester pinned to 5.x: the suite is written against the v5 configuration
|
||||
# API (New-PesterConfiguration), and Windows images still carry a v3 in
|
||||
# the module path that would be picked ahead of it. PSScriptAnalyzer is
|
||||
# the PowerShell linter; there is no typecheck for PowerShell, so the
|
||||
# analyzer is the whole of that half.
|
||||
- name: Install Pester 5 and PSScriptAnalyzer
|
||||
shell: powershell
|
||||
run: |
|
||||
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
|
||||
Install-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 `
|
||||
-Force -SkipPublisherCheck -Scope CurrentUser
|
||||
Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
|
||||
Import-Module Pester -MinimumVersion 5.5.0
|
||||
'Pester ' + (Get-Module Pester).Version + ', PSScriptAnalyzer ' + (Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1).Version
|
||||
|
||||
# Errors fail the job; warnings are printed and do not. The repo was
|
||||
# written without the analyzer, and turning every style warning into a
|
||||
# red build on day one would make the gate something to disable rather
|
||||
# than something to keep. PSAvoidUsingWriteHost is excluded outright:
|
||||
# these are command-line tools whose Write-Host output IS the interface.
|
||||
- name: Lint (PSScriptAnalyzer)
|
||||
shell: powershell
|
||||
run: |
|
||||
$r = Invoke-ScriptAnalyzer -Path . -Recurse -ExcludeRule PSAvoidUsingWriteHost
|
||||
$warn = @($r | Where-Object Severity -eq Warning)
|
||||
$err = @($r | Where-Object Severity -eq Error)
|
||||
if ($warn) {
|
||||
Write-Host ("{0} warning(s), not failing the build:" -f $warn.Count)
|
||||
$warn | Format-Table RuleName, ScriptName, Line -AutoSize | Out-String | Write-Host
|
||||
}
|
||||
if ($err) {
|
||||
$err | Format-Table RuleName, ScriptName, Line, Message -AutoSize -Wrap | Out-String | Write-Host
|
||||
throw ("PSScriptAnalyzer: {0} error(s)" -f $err.Count)
|
||||
}
|
||||
Write-Host "no errors"
|
||||
|
||||
# -CI sets the exit code from the result, which is the whole point here:
|
||||
# Invoke-Pester on its own reports failures and still exits 0, so the
|
||||
# job would go green with a red suite.
|
||||
- name: Tests
|
||||
shell: powershell
|
||||
run: Invoke-Pester -Path tests -CI
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,3 +19,6 @@ md/
|
||||
|
||||
# Pester coverage output (regenerated; never committed)
|
||||
coverage/
|
||||
|
||||
# Generated by scripts/plaintext_twins.py
|
||||
__pycache__/
|
||||
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@ -1,15 +1,101 @@
|
||||
<!--
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
Notable changes to the Evomedia.net Token Savers.
|
||||
Notable changes to the evomedia.net Token Savers.
|
||||
|
||||
## Unreleased
|
||||
|
||||
## v1.0.0.0.28 - 2026-09-22
|
||||
|
||||
### Changed
|
||||
|
||||
- **`zec2` and `zec2online` comments now say what they mean without
|
||||
naming private detail.** The container-side version read is described
|
||||
by what it is - an endpoint that is not public on every project - rather
|
||||
than by a product's own wording, and `zec2` records why it needed its
|
||||
own ssh: the read referenced three variables the script never defined,
|
||||
and because it sits inside a try/catch the failure was silent and looked
|
||||
exactly like a service that could not be reached.
|
||||
|
||||
## v1.0.0.0.26 - 2026-09-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`zdeploy` on a docker stack built from source shipped nothing after the
|
||||
first deploy.** The docker kind ran `docker compose pull` and then
|
||||
`docker compose up -d`, which is right for a stack of published images and
|
||||
wrong for one built from a `Dockerfile` in the tree: there is nothing to
|
||||
pull, and `up -d` builds only when the image is *missing*. So the first
|
||||
deploy worked and every one after it uploaded the new code, started the old
|
||||
image, and reported success — worse than an error, because the deploy is
|
||||
green and the container is healthy. A project now opts into building with
|
||||
`"deploy": { "build": true }`, which runs `docker compose build --pull` so
|
||||
the base image is refreshed at the same time. Stacks that pull are
|
||||
unaffected.
|
||||
|
||||
## v1.0.0.0.25 - 2026-09-12
|
||||
|
||||
### Added
|
||||
|
||||
- **`zmerge`** — merge every pull request across the org that is genuinely
|
||||
ready (`MERGEABLE` / `CLEAN`, not a draft), re-checking each one immediately
|
||||
before and after every merge, because merging into a default branch can
|
||||
conflict a sibling PR in the same repository. Dry run by default;
|
||||
`-Execute` (or `-e`) merges.
|
||||
- **`zpull`** — `zmerge`, then `git pull --ff-only` in every checkout the
|
||||
merges affected. Skips a checkout that is dirty or is not on its default
|
||||
branch rather than guessing.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`-e` is an alias for `-Execute`** on both of the above, the way `-s`
|
||||
already works for `-Scan`.
|
||||
- **`zmerge` discovers repositories instead of listing them.** It asked a
|
||||
hand-kept list, which had fallen well behind the org - so a scan covered
|
||||
about half of it and reported "Nothing open to merge" while a ready pull
|
||||
request sat in a repository the list had never heard of. It now asks GitHub,
|
||||
and throws rather than returning an empty list if that fails: a tool that
|
||||
quietly scans nothing prints the same reassuring line as one that scanned
|
||||
everything, and the two must not be confusable.
|
||||
|
||||
## v1.0.0.0.24 - 2026-09-08
|
||||
|
||||
### Added
|
||||
- **`zdeploy` can lay the release tag it already knows the number for.**
|
||||
A versioning scheme that asks every release to carry an annotated tag needs
|
||||
something to enforce it, and for a project whose build number lives outside
|
||||
git - in a database, say - nothing did: one project reached thirty-eight
|
||||
builds with four tags, and the missing ones were unrecoverable because the
|
||||
number had never existed anywhere else. With `deploy.tagOnDeploy`, the
|
||||
deployed commit is tagged with its build number and pushed, but only after
|
||||
the live build has been *verified* - a tag is a claim about what is running.
|
||||
Opt-in, because a project that already tags releases through a pull request
|
||||
must not also collect a tag per deploy. It can never fail a deploy: an
|
||||
existing tag is left alone, a failed push keeps the tag local and prints the
|
||||
command to finish it, and a missing repo just says so.
|
||||
|
||||
### Fixed
|
||||
- **The mirror stopped publishing current product names.** Its own denylist
|
||||
never saw the current spellings (a dot or hyphen breaks the word, an
|
||||
underscore hides the boundary), so twelve references went out while the
|
||||
suite ran green. The patterns learn the spellings, planted cases prove it,
|
||||
and the references read generically now.
|
||||
- **`zstart` no longer aborts on a pull that succeeded.** git reports
|
||||
ordinary fetch progress (`From https://...`) on stderr, and under Windows
|
||||
PowerShell 5.1 the script's `2>&1` turned that into a terminating error -
|
||||
so a pull that had *worked* stopped the dev server from starting, before
|
||||
the script's own "Auto-pull skipped" branch could run. The pull now lives
|
||||
in `Invoke-StartGitPull`, which never throws, never switches branch, and
|
||||
never touches a dirty tree: it fast-forwards when it can, reports when it
|
||||
can't, and `zstart` carries on either way - the opposite failure mode
|
||||
from `Invoke-DeployGitPull`, on purpose. Fourteen tests drive real git
|
||||
under `Stop` on 5.1, the host the defect lives on (#130).
|
||||
|
||||
## v1.0.0.0.23 - 2026-08-31
|
||||
|
||||
### Changed
|
||||
|
||||
100
CHANGELOG.txt
100
CHANGELOG.txt
@ -1,15 +1,111 @@
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
Notable changes to the Evomedia.net Token Savers.
|
||||
Notable changes to the evomedia.net Token Savers.
|
||||
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
v1.0.0.0.28 - 2026-09-22
|
||||
------------------------
|
||||
|
||||
Changed
|
||||
-------
|
||||
|
||||
- **zec2 and zec2online comments now say what they mean without
|
||||
naming private detail.** The container-side version read is described
|
||||
by what it is - an endpoint that is not public on every project - rather
|
||||
than by a product's own wording, and zec2 records why it needed its
|
||||
own ssh: the read referenced three variables the script never defined,
|
||||
and because it sits inside a try/catch the failure was silent and looked
|
||||
exactly like a service that could not be reached.
|
||||
|
||||
v1.0.0.0.26 - 2026-09-14
|
||||
------------------------
|
||||
|
||||
Fixed
|
||||
-----
|
||||
|
||||
- **zdeploy on a docker stack built from source shipped nothing after the
|
||||
first deploy.** The docker kind ran docker compose pull and then
|
||||
docker compose up -d, which is right for a stack of published images and
|
||||
wrong for one built from a Dockerfile in the tree: there is nothing to
|
||||
pull, and up -d builds only when the image is missing. So the first
|
||||
deploy worked and every one after it uploaded the new code, started the old
|
||||
image, and reported success — worse than an error, because the deploy is
|
||||
green and the container is healthy. A project now opts into building with
|
||||
"deploy": { "build": true }, which runs docker compose build --pull so
|
||||
the base image is refreshed at the same time. Stacks that pull are
|
||||
unaffected.
|
||||
|
||||
v1.0.0.0.25 - 2026-09-12
|
||||
------------------------
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
- zmerge — merge every pull request across the org that is genuinely
|
||||
ready (MERGEABLE / CLEAN, not a draft), re-checking each one immediately
|
||||
before and after every merge, because merging into a default branch can
|
||||
conflict a sibling PR in the same repository. Dry run by default;
|
||||
-Execute (or -e) merges.
|
||||
- zpull — zmerge, then git pull --ff-only in every checkout the
|
||||
merges affected. Skips a checkout that is dirty or is not on its default
|
||||
branch rather than guessing.
|
||||
|
||||
Changed
|
||||
-------
|
||||
|
||||
- -e is an alias for -Execute on both of the above, the way -s
|
||||
already works for -Scan.
|
||||
- zmerge discovers repositories instead of listing them. It asked a
|
||||
hand-kept list, which had fallen well behind the org - so a scan covered
|
||||
about half of it and reported "Nothing open to merge" while a ready pull
|
||||
request sat in a repository the list had never heard of. It now asks GitHub,
|
||||
and throws rather than returning an empty list if that fails: a tool that
|
||||
quietly scans nothing prints the same reassuring line as one that scanned
|
||||
everything, and the two must not be confusable.
|
||||
|
||||
v1.0.0.0.24 - 2026-09-08
|
||||
------------------------
|
||||
|
||||
Added
|
||||
-----
|
||||
- zdeploy can lay the release tag it already knows the number for.
|
||||
A versioning scheme that asks every release to carry an annotated tag needs
|
||||
something to enforce it, and for a project whose build number lives outside
|
||||
git - in a database, say - nothing did: one project reached thirty-eight
|
||||
builds with four tags, and the missing ones were unrecoverable because the
|
||||
number had never existed anywhere else. With deploy.tagOnDeploy, the
|
||||
deployed commit is tagged with its build number and pushed, but only after
|
||||
the live build has been verified - a tag is a claim about what is running.
|
||||
Opt-in, because a project that already tags releases through a pull request
|
||||
must not also collect a tag per deploy. It can never fail a deploy: an
|
||||
existing tag is left alone, a failed push keeps the tag local and prints the
|
||||
command to finish it, and a missing repo just says so.
|
||||
|
||||
Fixed
|
||||
-----
|
||||
- The mirror stopped publishing current product names. Its own denylist
|
||||
never saw the current spellings (a dot or hyphen breaks the word, an
|
||||
underscore hides the boundary), so twelve references went out while the
|
||||
suite ran green. The patterns learn the spellings, planted cases prove it,
|
||||
and the references read generically now.
|
||||
- zstart no longer aborts on a pull that succeeded. git reports
|
||||
ordinary fetch progress (From https://...) on stderr, and under Windows
|
||||
PowerShell 5.1 the script's 2>&1 turned that into a terminating error -
|
||||
so a pull that had worked stopped the dev server from starting, before
|
||||
the script's own "Auto-pull skipped" branch could run. The pull now lives
|
||||
in Invoke-StartGitPull, which never throws, never switches branch, and
|
||||
never touches a dirty tree: it fast-forwards when it can, reports when it
|
||||
can't, and zstart carries on either way - the opposite failure mode
|
||||
from Invoke-DeployGitPull, on purpose. Fourteen tests drive real git
|
||||
under Stop on 5.1, the host the defect lives on (#130).
|
||||
|
||||
v1.0.0.0.23 - 2026-08-31
|
||||
------------------------
|
||||
|
||||
|
||||
@ -1,42 +1,45 @@
|
||||
cd9613905a398e3e280d0ce82bdf58290b725f9359abc6e4e6bf2789b0d525cc setup_backup_schedule.ps1
|
||||
a4ef31101c6d935955b07525ec41645d2c194b517941a2faa32e1f07b81bd380 token-count.ps1
|
||||
d9961827daa176cd2600d048dd2a2dcdad8e816099e8dc0a9c472b0b28f1ea37 zbackup.cmd
|
||||
cf18bd9021aee57600fd52c396f1499663a398070661b2e948a0409c006fd3b3 zbackup.ps1
|
||||
e59bcc1ce4b304ea399bd107d8894e54ac428d20c300ec9488486d3ec6507615 zbackup_and_sync.ps1
|
||||
cefe3201301d1bee8aa1cdd35818123836eda4d6975eb0f1d46352f45a484d35 zbackup_ec2.cmd
|
||||
93cb114cd1689bf44dcf9eecc4a0eed71315b7b81111230d531a77a6e3dfe896 zbackup_ec2.ps1
|
||||
7dae9f0f531cdd1a1ba46f9c52c41d26d2cf1168841c247d71dbea09362127b0 zchecksums.cmd
|
||||
3d4bb2eee3aea0d8416b027a621e3e82b54c257a2cd694d4af65d00f828e1a3f zchecksums.ps1
|
||||
6a5dd5f658338a44eae25379e51714550503cd0b85bb494a10af20138a0e948c zdeploy.cmd
|
||||
258ce6a81bd75016fe430bd48630c110fda08a0f45256caa071245dab0f8c36d zdeploy.ps1
|
||||
c7bca9f557a816c2db5805fe4d0e90a4784976d7e2a177e3fa14b45465546702 zec2.cmd
|
||||
bb5b4d306ef2d4a4a02b77546052259ad6895b339741d8da112a5a2252e95ba1 zec2.ps1
|
||||
73486d8df9a0afb5a87efb580036e71878c496c11f8074d12ccaad5ddc572393 zec2_rotatekeys.cmd
|
||||
8cca7a1977bec02f569b78b8de1470307ffe62c4590443270704227671b4667a zec2_rotatekeys.ps1
|
||||
f10e9d1ec91098e7e736bfbd57ad74d80a56b1121e34bdc381c0bfd2dd603007 zec2online.cmd
|
||||
259b11058582b505a9cf8e0fd3aaff475ce4c131d40c8dfa00205090ad0a68c4 zec2online.ps1
|
||||
57cbfc44c5c179d64c6fc4ea3f2688ea79dfc5881ac98bf87c4b3ad54b6f2ce8 ZHelpers.ps1
|
||||
4269c1850b05f2c13cfb0545e8e6f28429b6b7da7e986f766f1ba2f7bf2c9535 zkill.cmd
|
||||
2c592ace4f565e9cedc9e296c42132115ea9ba8963f1718a6b85aafb76981702 zkill.ps1
|
||||
e8f3c7207d68cf291a1e83a55dd8d49d71a68980830e5d4f6d89b07012cac314 ZKiller.ps1
|
||||
65ad2b013b623f6a3d22961414e1dc97e563fdc60655bbef47c2144a3298f210 ZKillOnly.ps1
|
||||
aea2f476314e68ab4c7053c56831ec108457e0802cb763abca090adca04b56b8 zrelease.cmd
|
||||
919f55a7f137791567f077635e9e2afcd60185aa1f9d7324d345d37ac9a638b5 zrelease.ps1
|
||||
48f0e798023680f95ad269f7f0b5270d0fce7fadcb1cc68f8c52fb72c3334abc zrepair.cmd
|
||||
58d7946c20bda1586d7b9e385e08e48b3f2867b622cffe4524a841ea3460307c zrepair.ps1
|
||||
fe0c9bcbbff9b51b96a160ae8eb692945d546e3d5902c1dd1c2e3238a4b76456 zrestart.cmd
|
||||
796118c8880b1fec5b0b6930c0670e1e1478c90aeec8e22332a9fc1f6af55c35 zrestart.ps1
|
||||
f9efb2777e4463c131bf88c267082c307b063047bb3c2817808f5b7bf2aaf328 zrestartd.cmd
|
||||
72c3f10ee772310f7aadc76f3f2be0feb7a9f6cfae062aab7e1734bdf44f2951 zsetup.cmd
|
||||
d42ec4c112aa0aa136d2dfb8d4a3ca2533b14be3370ce1d025a39ae64049fcc0 zsetup.ps1
|
||||
7ad635ee4a43de1c2a7cd4d4e0086b94cdda6e8483ace046718032282a668d9a zsetup_mail.ps1
|
||||
22b022af72e8fc621aed59d2ed351f5bb1e12f336bb626d986c0e7dd578dff68 zstart.cmd
|
||||
f610ea73419b1c8dccd5a6e6d86392a990dbfb152f390439d5e91fea2e4e0693 zstart.ps1
|
||||
45b6dd4ce6f1a3459a1c7bd71e07ac7883b5ac57b403bd7b754c517bfcde3e78 zstart_docker.cmd
|
||||
2e50d20697f84860d55ec22dc050cdd7fb494b40eacf022f321cefbaa2806c3e zstart_docker.ps1
|
||||
ceced5153e51daddd2316e538d23d1cfad01ff19fbbca619119acd1628a37eb5 zstartd.cmd
|
||||
ecf38284f4341c122f88c7ab5ece0a205d69f8415e27587159cb5e20a0830d72 zstop.ps1
|
||||
a5e4b4ee6597422394839e1438832a82468b56ec2d7f2b1110a28fa077562a4b zsync.cmd
|
||||
6a7df5043fd5e2b8edbfd9f9806d48b85a0555ba77c10d06100fa45d390eacf9 zsync.ps1
|
||||
e7e4c5c4eef0e0d45ab285bc3619a61ed75b1fbc4060a545dbc83d27d89aa238 zversion.cmd
|
||||
1f18c4ac1251369f65aa6ab97d3edb89e0e79e085af89aeeae81c306cb5a43f3 zversion.ps1
|
||||
6e95736007b3906950d95a830705ac1118ebfc11836d2c717d6dd49cb3157966 setup_backup_schedule.ps1
|
||||
d6ab2ddb273a8ca870bd0673cffdd0907f16f4718367b4413a3e34bb1f1769c8 token-count.ps1
|
||||
30ccf43c371ae95cecd5b443d9214ac8ea71ec83be94bc15bcf359beefb8dee1 zbackup.cmd
|
||||
b3549b346c90a8ae5a05c4c91b7ac370f72467c4cb0d019edcd0ea80b994393e zbackup.ps1
|
||||
b6e5e15f8e2b128219c7b8b9db3e9b7804eee60fd6aed43f184210048e518f3c zbackup_and_sync.ps1
|
||||
b407ef5b298cd6fe94af492e9254b4447e34333d22f8c29d3e9d4d2881651cf3 zbackup_ec2.cmd
|
||||
55df5f2e19e81317b1e33b7b030b252fc4d6f49c0c77229d9e4e86affeb3b8ef zbackup_ec2.ps1
|
||||
64bc3148b09e422ffe4890339fade2354a31f2c7ba004f2728c29a0a39c32f08 zchecksums.cmd
|
||||
3e1c573e446238cae494d22d25376f278333dc25fdbc0a5abbdaa9349722b438 zchecksums.ps1
|
||||
049f8e79fc8c3d8d96ae8ccb1155d191e2dbf2753f4d4d73ceca00aa1ab0481b zdeploy.cmd
|
||||
d2ead96436507c8efedc3c72045e623137a1f844940c4ff57c9c7ebe67645511 zdeploy.ps1
|
||||
30918a9260cb6220d6e2140edcf319fc0fa1e75b7fe36d0efe68b0d10cdde241 zec2.cmd
|
||||
695ec87ea6518933ee64524d369f1d0d5128bf6e54db31f555a888bdddcb9326 zec2.ps1
|
||||
ce3209ed8eaab242286e76aeb11b1249239e2804e3de456a1609cc7caa780b43 zec2_rotatekeys.cmd
|
||||
f58277779b5c54814c29dda55ab567960e305fa9ef515b1206076be97f623093 zec2_rotatekeys.ps1
|
||||
30ed73711eeddb518e02eb5e1968b7cc1dad51f27ecf42155b515b097487c3f3 zec2online.cmd
|
||||
1e8814928910ff4f3da59478511feb75280ba2e12111840fb47d2c5504741f0f zec2online.ps1
|
||||
4ea81dc9e3cafc514b1e8224e633bf22bd88bfdc6e53545ff88a2a2d33715b4f ZHelpers.ps1
|
||||
d97f9b5c68526c3295796b3042ee86194721eacab56b0efa0f3fdf16c881b0ea zkill.cmd
|
||||
587de0b176788de917e713915ac468e44bf00cf9c9c7d6ea9b88aaec41ad6ea7 zkill.ps1
|
||||
66556f55688d1f31e890d2b36143d3d0e2f5fcdded562e3d0a9591c02e827cda ZKiller.ps1
|
||||
2c9e0fa5dabb1544b6600cb60e5f66e89c787f217db246553b830152ab976ec3 ZKillOnly.ps1
|
||||
479d01a4c962eb1dba2dbfee913c3704048f29581a0a06d068c0fb9145c3a51c zmerge.ps1
|
||||
dd1cfde33aec53fc0df4a34e45704a59bf48e094fcafcc0ec49e9d13aa442b6f zpull.cmd
|
||||
9e746ad18b92ee7344061847b38c870f280421cf55d31e36a72a1514772e53a2 zpull.ps1
|
||||
4a57e270fc75ae5419bd27cf01b7dd6d8965be8dd58e0c80af985cb7bf27bdc5 zrelease.cmd
|
||||
a6906e118f13442340cc4e0b14d7523d63e85d2bdb0eff8fd3a7c25567af8962 zrelease.ps1
|
||||
58e5b604cc260af7e644412094ff6e3bf799f80f9e52b9a83a044299f7704107 zrepair.cmd
|
||||
186f7b0fb72808466ece328ea1a4e3a97bb2d4bae9f204ff7055e8b7f009614b zrepair.ps1
|
||||
d97c9cf55b94c53dba49471d13f5c9870f2f50627ac5732ac4b60222da936244 zrestart.cmd
|
||||
32a8484d356c5f740d7a12f18bf0dd51a0c7da66010f6a6f553d976484315eef zrestart.ps1
|
||||
2ebf37f72323cac3963a4a95d1ae414fecf4da1e1e618147663c6cf94d1ece24 zrestartd.cmd
|
||||
0a15ad0d341a5efdcd4c14e22e91d4b45f2bf87d0c5608fcf985200f6797bab4 zsetup.cmd
|
||||
0d3042c5e44c3edf396005d0177f744c0c29536c25a50743e93a92156aa2a96c zsetup.ps1
|
||||
498ad7f8a7a56b332fede299e7172211ba360e76d9244c6c93f7cceae8a58619 zsetup_mail.ps1
|
||||
46b3782d9b05649bdb1bcbd2a007bf6abc889abee420ac2f6e75530fa45a4694 zstart.cmd
|
||||
4b50cadf761f285f4fe655b143e04ec6b3565646ee8fb1ed165c234a201802b7 zstart.ps1
|
||||
2d74dcca3d4f51a28c84fbef5cc134126b61dfcc8ffb35e2d72f9596bf685809 zstart_docker.cmd
|
||||
5016654d9ad68e11b85594be5e05318e19c5c6154174f734c43251970bc2086f zstart_docker.ps1
|
||||
1e1c5990e1dba41a165e797790f73cd33fcee5035eec65dd0d2610ea8b56a023 zstartd.cmd
|
||||
a894876ad9fa1bfac6cacfec2a837914debb773e05b975d9b3cdf56589dd8ed2 zstop.ps1
|
||||
2e165d35d9915099b98a14329c9eb2774b2bf2979c937bc3843d95b5b16400e7 zsync.cmd
|
||||
5d008cc252b978a9f873101139054988dff24562655bdfcd22e781aa5c14029a zsync.ps1
|
||||
d55020eeb926e7190d06cc3de3e4a0d0656e8c73e8a89032c06ffe29c0b661eb zversion.cmd
|
||||
25de4c34219edb98b583be639b798b27c353e978a81f0480d49ba9d965f70f0b zversion.ps1
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
38
ELEVATOR_PITCH.txt
Normal file
38
ELEVATOR_PITCH.txt
Normal file
@ -0,0 +1,38 @@
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
Elevator Pitch
|
||||
==============
|
||||
|
||||
The one-liner
|
||||
-------------
|
||||
|
||||
AI coding agents waste thousands of tokens a day on infrastructure orchestration. Token Savers gives you one-word commands to run those parts yourself — so your agent spends tokens on code, not on SSH.
|
||||
|
||||
The 30-second version
|
||||
---------------------
|
||||
|
||||
Every time your AI coding agent runs your infrastructure for you — a deploy, a restart, a health check — the full output lands in its context window: Docker layers, SSH banners, health-check chatter. We measured it per run — at a typical active-day cadence that's ~26,500 tokens of pure script output through the agent, and a single full Docker rebuild adds ~35,000 more. The dollars are small; the context is not — every line of infrastructure noise crowds out the code your agent is supposed to be reasoning about.
|
||||
|
||||
Token Savers collapses the infrastructure side into short, one-word commands you run yourself: zdeploy myapp, zrepair myapp, zstart myapp. Describe each project once in zconfig.json — where it lives, what kind it is, where it deploys — and every command just knows. You run the deploy; your agent edits the code. You run the health check; your agent reads the result and fixes whatever's wrong.
|
||||
|
||||
Measured per-run; ~26,500 tokens of script output per active development day at a typical cadence — kept out of your agent's context entirely when you run the commands yourself. See TOKEN_SAVINGS.md (TOKEN_SAVINGS.md) for the per-script measurements and method.
|
||||
|
||||
Why it's different
|
||||
------------------
|
||||
|
||||
- Built around the AI-agent workflow. The commands are short on purpose — fewer keystrokes for you, fewer tokens when an agent invokes them. But the real saving is the operations you don't hand to the agent at all.
|
||||
- The project name IS the command. zstart blog, zdeploy api, zbackup store — no flags to memorize, no switches to wire up.
|
||||
- One config file, zero secrets in git. Server IP, SSH key, paths, and project definitions live in one gitignored JSON. Clone it anywhere, drop in your config, go.
|
||||
- It verifies the deploy actually landed. Not "did the server return 200" (a stale cache does that too) — it checks that the build number went live, so you know the code you just shipped is the code that's running.
|
||||
|
||||
Who it's for
|
||||
------------
|
||||
|
||||
Solo developers and small teams running several containerized web apps (Python, Vite, Next.js, plus edge proxies and stock Docker images) on a single VPS or EC2 box, from a Windows dev machine, over SSH — and using AI coding agents to write the code.
|
||||
|
||||
The tagline
|
||||
-----------
|
||||
|
||||
Fewer keystrokes. Fewer tokens. One config to rule your fleet.
|
||||
26
README.md
26
README.md
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
@ -91,6 +91,7 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else — h
|
||||
"deploy": {
|
||||
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
||||
"gitPull": true, // optional: git pull --ff-only before zipping
|
||||
"tagOnDeploy": false, // optional: tag the deployed commit with its build number
|
||||
"exclude": ["docs", "big-data-folder"] // optional: extra top-level dirs/files to skip
|
||||
}
|
||||
}
|
||||
@ -101,9 +102,10 @@ Set the `ZCONFIG` environment variable to point at a config somewhere else — h
|
||||
Optional blocks do real work:
|
||||
|
||||
- **`install`** — how `zsetup` installs a python project's dependencies into its `.venv`: the pip args, e.g. `"-e ."`, `"-e backend"` (deps in a subfolder), or `"-r requirements.txt"`. Omit it and `zsetup` auto-detects a root `pyproject.toml`/`setup.py` (`-e .`) or `requirements.txt` (`-r requirements.txt`). `zstart` never installs — run `zsetup <key>` once, then `zstart <key>`.
|
||||
- **`start`** — pre-start steps for `zstart`: `gitPull: true` runs `git pull --ff-only` in the project root first (never starts a stale checkout), and `env` sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- **`start`** — pre-start steps for `zstart`: `gitPull: true` fast-forwards the checkout from its upstream first, and when it can't — no upstream, diverged history, a remote that wants credentials — says so and starts the server anyway. A pull is never allowed to stand between you and a running dev server (that is `deploy.gitPull`'s job, below, where refusing is correct). `env` sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- **`db`** — deploys wait for `pg_isready` and `zbackup_ec2` pulls a `pg_dump`, both against the compose service named `db`. Omit it and those steps are skipped cleanly.
|
||||
- **`deploy.gitPull`** — `git pull --ff-only` in the project root before zipping, so a merged PR actually ships. Since `zdeploy` zips your working tree, a checkout left behind `origin` would otherwise deploy stale code *and still bump the build number* — a silent no-op that looks like success. A failed pull (dirty tree that conflicts, diverged history) aborts the deploy rather than shipping uncertain code.
|
||||
- **`deploy.tagOnDeploy`** — after a deploy whose live build number has been *verified*, lay an annotated git tag for that number on the deployed commit and push it. Off by default, and deliberately opt-in: a project that already tags its releases through a pull request must not also collect a tag per deploy, because a release ledger and a deploy counter are two different numbers. Nothing here can fail a deploy — an existing tag is left alone, a failed push leaves the tag local and tells you the command to finish it, and a working tree with uncommitted changes gets a warning that the tagged commit is not everything that shipped.
|
||||
- **`migrations": "prisma"`** — runs `npx prisma migrate deploy` inside the app container after each deploy.
|
||||
- **Compose service-name conventions** — handlers assume the app service is named `app` (python) or `web` (nextjs) and the database service `db`. Override the app service with `remote.appService`.
|
||||
- **Edge extras** — an `edge`-kind project can set `proxyContainer` (the nginx container's name, used for reloads and stale-container cleanup) and `certsSource` (a host path with TLS certs, mounted read-only when validating `nginx.conf`).
|
||||
@ -134,6 +136,8 @@ The `.cmd` wrappers are the everyday interface. Every command takes one or more
|
||||
| `zchecksums [-Update]` | Verify every script against `CHECKSUMS.txt` (SHA-256) |
|
||||
| `zversion [bump \| bump-stage <s> \| set <v>]` | Show or advance the toolkit version (stamps every header) |
|
||||
| `zrelease [-Verify]` | Package the current version as `releases/zscripts-<version>.zip` + `.sha256` |
|
||||
| `zmerge [-Execute\|-e]` | Merge every pull request across the org that is genuinely ready |
|
||||
| `zpull [-Execute\|-e]` | `zmerge`, then bring every affected local checkout current |
|
||||
|
||||
### Local development
|
||||
|
||||
@ -407,6 +411,24 @@ Verification walks its channels in trust order — docker-network `viaProxy`, th
|
||||
|
||||
Each release is packaged as a zip in [`releases/`](releases/) — grab the latest `zscripts-v*.zip`, check it, unzip, done:
|
||||
|
||||
**Windows · PowerShell** — these commands are a PowerShell toolkit, so this is
|
||||
most people's path. `sha256sum` and `unzip` are not Windows commands:
|
||||
|
||||
```powershell
|
||||
$zip = "zscripts-v1.0.0.0.0.zip"
|
||||
(Get-FileHash $zip -Algorithm SHA256).Hash -eq (Get-Content "$zip.sha256").Split()[0] # True = good
|
||||
Expand-Archive $zip -DestinationPath zscripts
|
||||
cd zscripts
|
||||
.\zchecksums.cmd # verify the contents
|
||||
```
|
||||
|
||||
`Get-FileHash` prints the hash in **upper** case and the `.sha256` file holds it
|
||||
in lower — they look different side by side and are not. `-eq` on strings is
|
||||
case-insensitive in PowerShell, so the comparison above is right; trust the
|
||||
`True`, not your eyes.
|
||||
|
||||
**macOS · Linux · Git Bash · WSL**
|
||||
|
||||
```bash
|
||||
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
|
||||
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
||||
|
||||
24
README.txt
24
README.txt
@ -1,4 +1,4 @@
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -88,6 +88,7 @@ Config reference
|
||||
"deploy": {
|
||||
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
||||
"gitPull": true, // optional: git pull --ff-only before zipping
|
||||
"tagOnDeploy": false, // optional: tag the deployed commit with its build number
|
||||
"exclude": ["docs", "big-data-folder"] // optional: extra top-level dirs/files to skip
|
||||
}
|
||||
}
|
||||
@ -97,9 +98,10 @@ Config reference
|
||||
Optional blocks do real work:
|
||||
|
||||
- install — how zsetup installs a python project's dependencies into its .venv: the pip args, e.g. "-e .", "-e backend" (deps in a subfolder), or "-r requirements.txt". Omit it and zsetup auto-detects a root pyproject.toml/setup.py (-e .) or requirements.txt (-r requirements.txt). zstart never installs — run zsetup <key> once, then zstart <key>.
|
||||
- start — pre-start steps for zstart: gitPull: true runs git pull --ff-only in the project root first (never starts a stale checkout), and env sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- start — pre-start steps for zstart: gitPull: true fast-forwards the checkout from its upstream first, and when it can't — no upstream, diverged history, a remote that wants credentials — says so and starts the server anyway. A pull is never allowed to stand between you and a running dev server (that is deploy.gitPull's job, below, where refusing is correct). env sets environment variables for the dev-server process (feature flags, reload switches).
|
||||
- db — deploys wait for pg_isready and zbackup_ec2 pulls a pg_dump, both against the compose service named db. Omit it and those steps are skipped cleanly.
|
||||
- deploy.gitPull — git pull --ff-only in the project root before zipping, so a merged PR actually ships. Since zdeploy zips your working tree, a checkout left behind origin would otherwise deploy stale code and still bump the build number — a silent no-op that looks like success. A failed pull (dirty tree that conflicts, diverged history) aborts the deploy rather than shipping uncertain code.
|
||||
- deploy.tagOnDeploy — after a deploy whose live build number has been verified, lay an annotated git tag for that number on the deployed commit and push it. Off by default, and deliberately opt-in: a project that already tags its releases through a pull request must not also collect a tag per deploy, because a release ledger and a deploy counter are two different numbers. Nothing here can fail a deploy — an existing tag is left alone, a failed push leaves the tag local and tells you the command to finish it, and a working tree with uncommitted changes gets a warning that the tagged commit is not everything that shipped.
|
||||
- migrations": "prisma" — runs npx prisma migrate deploy inside the app container after each deploy.
|
||||
- Compose service-name conventions — handlers assume the app service is named app (python) or web (nextjs) and the database service db. Override the app service with remote.appService.
|
||||
- Edge extras — an edge-kind project can set proxyContainer (the nginx container's name, used for reloads and stale-container cleanup) and certsSource (a host path with TLS certs, mounted read-only when validating nginx.conf).
|
||||
@ -131,6 +133,8 @@ The .cmd wrappers are the everyday interface. Every command takes one or more pr
|
||||
| zchecksums [-Update] | Verify every script against CHECKSUMS.txt (SHA-256) |
|
||||
| zversion [bump \| bump-stage <s> \| set <v>] | Show or advance the toolkit version (stamps every header) |
|
||||
| zrelease [-Verify] | Package the current version as releases/zscripts-<version>.zip + .sha256 |
|
||||
| zmerge [-Execute\|-e] | Merge every pull request across the org that is genuinely ready |
|
||||
| zpull [-Execute\|-e] | zmerge, then bring every affected local checkout current |
|
||||
|
||||
Local development
|
||||
-----------------
|
||||
@ -380,6 +384,22 @@ Downloading without cloning
|
||||
|
||||
Each release is packaged as a zip in releases/ (releases/) — grab the latest zscripts-v*.zip, check it, unzip, done:
|
||||
|
||||
Windows · PowerShell — these commands are a PowerShell toolkit, so this is
|
||||
most people's path. sha256sum and unzip are not Windows commands:
|
||||
|
||||
$zip = "zscripts-v1.0.0.0.0.zip"
|
||||
(Get-FileHash $zip -Algorithm SHA256).Hash -eq (Get-Content "$zip.sha256").Split()[0] # True = good
|
||||
Expand-Archive $zip -DestinationPath zscripts
|
||||
cd zscripts
|
||||
.\zchecksums.cmd # verify the contents
|
||||
|
||||
Get-FileHash prints the hash in upper case and the .sha256 file holds it
|
||||
in lower — they look different side by side and are not. -eq on strings is
|
||||
case-insensitive in PowerShell, so the comparison above is right; trust the
|
||||
True, not your eyes.
|
||||
|
||||
macOS · Linux · Git Bash · WSL
|
||||
|
||||
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
|
||||
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
||||
cd zscripts && sha256sum -c CHECKSUMS.txt # verify the contents
|
||||
|
||||
68
SECURITY.md
Normal file
68
SECURITY.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Security
|
||||
|
||||
How to report a vulnerability, what to expect, and what is in scope:
|
||||
**[the evomedia-net security policy](https://github.com/evomedia-net/.github/blob/main/SECURITY.md)**.
|
||||
Short version — email [dev@evomedia.net](mailto:dev@evomedia.net), not a public
|
||||
issue.
|
||||
|
||||
What follows is particular to this repository, which is unusual in one way
|
||||
worth stating plainly.
|
||||
|
||||
## This is a mirror, and the interesting bug is a leak
|
||||
|
||||
These scripts are published from a private tree. The copy here is sanitised:
|
||||
placeholder hosts, example configuration, dummy data. So the most valuable
|
||||
thing anyone can report about this repository is not a crash — it is
|
||||
**something real that should not be here**:
|
||||
|
||||
- a credential, key, token, or private-key block
|
||||
- an internal hostname, a product domain, or an operator's path
|
||||
- an identifier that names a private project or a private-only script
|
||||
|
||||
If you find one, treat it as a live secret and mail
|
||||
[dev@evomedia.net](mailto:dev@evomedia.net) rather than opening an issue. A
|
||||
public issue about a leaked secret publishes it a second time and pins it to
|
||||
the top of the page.
|
||||
|
||||
**The automated check is a denylist.** `tests/Sanitization.Tests.ps1` and
|
||||
`tests/sanitization-patterns.psd1` hold the patterns this repository must never
|
||||
contain, and CI enforces them. A denylist proves the absence of *known*
|
||||
patterns, not the absence of secrets — it is a regression net for a specific
|
||||
recurring mistake, not a substitute for reading what is published. That is why
|
||||
a report here is worth sending even though the tests are green.
|
||||
|
||||
## They are automation scripts, so read them before running them
|
||||
|
||||
Everything here drives real infrastructure: archives a working tree, uploads
|
||||
it, rebuilds containers, restarts services. That is the purpose, and it means
|
||||
the ordinary rules for running someone else's shell scripts apply with more
|
||||
force than usual.
|
||||
|
||||
- **Read a script before the first run**, and run it against something you can
|
||||
afford to break.
|
||||
- **Nothing here is a sandbox.** There is no dry-run guarantee unless a script
|
||||
documents one; the flag that exists on one command may not exist on the next.
|
||||
- **The configuration is yours.** The example config carries placeholders, and
|
||||
every host, key path and target in it has to be replaced with your own before
|
||||
anything is pointed at real infrastructure.
|
||||
- Addresses in examples use the ranges reserved for documentation, and
|
||||
loopback. They are placeholders, not somewhere to send anything.
|
||||
|
||||
Scripts that destroy or overwrite state are the ones to read twice. A report
|
||||
that one of them does something destructive **without saying so** is a good
|
||||
report; a report that a script named after a destructive act performs it is
|
||||
not.
|
||||
|
||||
## Release integrity
|
||||
|
||||
Releases carry checksums. They are an **integrity check, not a signature** —
|
||||
they catch a truncated download, a corrupted mirror and an accidental edit,
|
||||
and they do not catch a forger, because whoever can change an archive can
|
||||
change the manifest that travels with it.
|
||||
|
||||
## Not a finding here
|
||||
|
||||
- **Placeholder credentials and example configuration.** Fake values are the
|
||||
sanitisation working, not a leak.
|
||||
- **The private tree.** Only what is published here is in scope; the internal
|
||||
original is not public and cannot be reviewed.
|
||||
73
SECURITY.txt
Normal file
73
SECURITY.txt
Normal file
@ -0,0 +1,73 @@
|
||||
Security
|
||||
========
|
||||
|
||||
How to report a vulnerability, what to expect, and what is in scope:
|
||||
the evomedia-net security policy (https://github.com/evomedia-net/.github/blob/main/SECURITY.md).
|
||||
Short version — email dev@evomedia.net (mailto:dev@evomedia.net), not a public
|
||||
issue.
|
||||
|
||||
What follows is particular to this repository, which is unusual in one way
|
||||
worth stating plainly.
|
||||
|
||||
This is a mirror, and the interesting bug is a leak
|
||||
---------------------------------------------------
|
||||
|
||||
These scripts are published from a private tree. The copy here is sanitised:
|
||||
placeholder hosts, example configuration, dummy data. So the most valuable
|
||||
thing anyone can report about this repository is not a crash — it is
|
||||
something real that should not be here:
|
||||
|
||||
- a credential, key, token, or private-key block
|
||||
- an internal hostname, a product domain, or an operator's path
|
||||
- an identifier that names a private project or a private-only script
|
||||
|
||||
If you find one, treat it as a live secret and mail
|
||||
dev@evomedia.net (mailto:dev@evomedia.net) rather than opening an issue. A
|
||||
public issue about a leaked secret publishes it a second time and pins it to
|
||||
the top of the page.
|
||||
|
||||
The automated check is a denylist. tests/Sanitization.Tests.ps1 and
|
||||
tests/sanitization-patterns.psd1 hold the patterns this repository must never
|
||||
contain, and CI enforces them. A denylist proves the absence of known
|
||||
patterns, not the absence of secrets — it is a regression net for a specific
|
||||
recurring mistake, not a substitute for reading what is published. That is why
|
||||
a report here is worth sending even though the tests are green.
|
||||
|
||||
They are automation scripts, so read them before running them
|
||||
-------------------------------------------------------------
|
||||
|
||||
Everything here drives real infrastructure: archives a working tree, uploads
|
||||
it, rebuilds containers, restarts services. That is the purpose, and it means
|
||||
the ordinary rules for running someone else's shell scripts apply with more
|
||||
force than usual.
|
||||
|
||||
- Read a script before the first run, and run it against something you can
|
||||
afford to break.
|
||||
- Nothing here is a sandbox. There is no dry-run guarantee unless a script
|
||||
documents one; the flag that exists on one command may not exist on the next.
|
||||
- The configuration is yours. The example config carries placeholders, and
|
||||
every host, key path and target in it has to be replaced with your own before
|
||||
anything is pointed at real infrastructure.
|
||||
- Addresses in examples use the ranges reserved for documentation, and
|
||||
loopback. They are placeholders, not somewhere to send anything.
|
||||
|
||||
Scripts that destroy or overwrite state are the ones to read twice. A report
|
||||
that one of them does something destructive without saying so is a good
|
||||
report; a report that a script named after a destructive act performs it is
|
||||
not.
|
||||
|
||||
Release integrity
|
||||
-----------------
|
||||
|
||||
Releases carry checksums. They are an integrity check, not a signature —
|
||||
they catch a truncated download, a corrupted mirror and an accidental edit,
|
||||
and they do not catch a forger, because whoever can change an archive can
|
||||
change the manifest that travels with it.
|
||||
|
||||
Not a finding here
|
||||
------------------
|
||||
|
||||
- Placeholder credentials and example configuration. Fake values are the
|
||||
sanitisation working, not a leak.
|
||||
- The private tree. Only what is published here is in scope; the internal
|
||||
original is not public and cannot be reviewed.
|
||||
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
-->
|
||||
|
||||
296
TOKEN_SAVINGS.txt
Normal file
296
TOKEN_SAVINGS.txt
Normal file
@ -0,0 +1,296 @@
|
||||
evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
Created by Kelly Michels · dev@evomedia.net
|
||||
Licensed under the MIT License. See LICENSE.
|
||||
|
||||
Token Savings: Why You Should Run These Scripts Yourself
|
||||
========================================================
|
||||
|
||||
Running these scripts manually keeps their output out of your AI coding agent's
|
||||
context window. Every line the agent doesn't have to read is a token you don't
|
||||
pay for — and a token the agent can spend on the actual problem instead of on
|
||||
deployment sequencing, SSH output, and Docker health checks.
|
||||
|
||||
This document reports two baselines side by side:
|
||||
|
||||
- You run it → Claude runs the script. What Claude ingests if it invokes the
|
||||
z-script as a single command. These figures are measured (see method below).
|
||||
- You run it → Claude orchestrates raw. What Claude would ingest if the scripts
|
||||
didn't exist and it drove scp / ssh / docker compose step by step itself.
|
||||
These figures are estimates — the same command output plus the agent's
|
||||
reasoning and retry logic across every discrete step.
|
||||
|
||||
The savings from running a script yourself is the first column: if you run it,
|
||||
Claude ingests zero. The extra value of having the scripts at all is the gap
|
||||
between the two columns.
|
||||
|
||||
Dollar equivalents use a blended input/output rate: Sonnet 5 ≈ $9/1M | Opus 4.8 ≈ $15/1M | Fable 5 ≈ $30/1M
|
||||
|
||||
> Measurement note: "Measured" figures come from token-count.ps1, which runs
|
||||
> each script under Start-Transcript and counts output characters ÷ 3.5
|
||||
> chars/token. Captured in Claude Code (Sonnet 4.6) against the sp project.
|
||||
> Strictly speaking that's measured output volume with estimated tokenization:
|
||||
> ÷3.5 is a prose heuristic, and code-heavy output (paths, JSON, container IDs)
|
||||
> fragments into more tokens per character under a real BPE tokenizer — so the
|
||||
> token figures here are likely conservative. "Estimated (raw)" figures are not
|
||||
> measured — they approximate manual orchestration and are marked est.
|
||||
> throughout. Other models/interfaces tokenize differently.
|
||||
|
||||
---
|
||||
|
||||
Measured per-run output (script-run baseline)
|
||||
---------------------------------------------
|
||||
|
||||
The bold figures below are real captures from token-count.ps1 sp; rows flagged est. are not:
|
||||
|
||||
| Script | Measured tokens/run | Notes |
|
||||
|--------|--------------------:|-------|
|
||||
| zec2online | 267 | reachability + version check |
|
||||
| zec2 | 331 | EC2 TCP/HTTP + build match |
|
||||
| zbackup_ec2 | 334 | pull server backup |
|
||||
| zrepair | 364 | clean audit; more if it restarts containers |
|
||||
| zkill | 377 | free the dev port |
|
||||
| zbackup | 436 | local project snapshot |
|
||||
| zrestart | 724 | kill + restart (detached) |
|
||||
| zstart | 762 | start dev server (detached) |
|
||||
| zsync | 769 | mirror backups offsite |
|
||||
| zdeploy (cached) | ~810 | 53s deploy, layers cached |
|
||||
| zdeploy (full rebuild) | ~34,600 est. | packages changed; streams full docker build |
|
||||
| zstart_docker | not measured | est. ~500–1,500 |
|
||||
|
||||
Cache state is what drives zdeploy. A cached deploy is ~810 tokens; the
|
||||
large number only appears on a full rebuild (dependencies changed), which streams
|
||||
the entire docker build. During rapid deploy → test → fix iteration almost every run
|
||||
is cached, so ~810 is the realistic per-run cost — with occasional spikes when you
|
||||
change packages.
|
||||
|
||||
---
|
||||
|
||||
Local Development Control
|
||||
-------------------------
|
||||
|
||||
zstart — Start dev servers
|
||||
--------------------------
|
||||
Measured: ~762 tokens/run | est. raw orchestration: ~1,500–3,000 | typical 2–3 runs/day
|
||||
|
||||
Run it yourself and Claude sees none of the version-bump, MOTD, and startup output.
|
||||
If Claude started the server raw, it would also wait on health checks and confirm
|
||||
the port is listening — reasoning the script does deterministically.
|
||||
|
||||
zstart viteapp # start Vite dev server on its configured port
|
||||
zstart pyapp -Port 3000 # override the port
|
||||
zstart nextapp -Detached # start in background, prompt returns
|
||||
|
||||
---
|
||||
|
||||
zkill — Stop dev servers
|
||||
------------------------
|
||||
Measured: ~377 tokens/run | est. raw orchestration: ~1,000–2,000 | typical 2–3 runs/day
|
||||
|
||||
Raw, Claude would enumerate processes, kill them, and re-check the port is free.
|
||||
The script collapses that to one command.
|
||||
|
||||
zkill viteapp
|
||||
zkill pyapp nextapp
|
||||
|
||||
---
|
||||
|
||||
zrestart — Restart in one command
|
||||
---------------------------------
|
||||
Measured: ~724 tokens/run | est. raw orchestration: ~2,500–4,500 | typical 10–15 runs/day
|
||||
|
||||
The most-used command during rapid iteration. Raw, it's stop → wait → start with
|
||||
error handling at each hop — several tool calls and their reasoning. As one script
|
||||
it's a single call, and the -Detached switch now propagates correctly through the
|
||||
kill→restart chain so the server backgrounds cleanly.
|
||||
|
||||
zrestart viteapp
|
||||
zrestart pyapp -Detached
|
||||
|
||||
---
|
||||
|
||||
Build & Deployment
|
||||
------------------
|
||||
|
||||
zdeploy — Deploy to EC2
|
||||
-----------------------
|
||||
Measured: ~810 tokens/run cached (spikes to ~34,600 on a full rebuild) | est. raw orchestration: ~5,000–12,000 cached, ~35,000+ full rebuild | typical 10–15 runs/day
|
||||
|
||||
The biggest lever — and the one where cache state matters most. The script streams
|
||||
the docker/SSH output whether Claude runs it or not, so a cached deploy really is only
|
||||
~810 tokens even through Claude. The raw-orchestration cost is higher not because of
|
||||
extra output but because Claude would reason between ~15 discrete steps (zip, preflight
|
||||
cleanup, scp, unzip, build, up, version bump, restart, verify) and handle retries
|
||||
itself. Running it yourself zeroes out all of that.
|
||||
|
||||
Measured cached: three runs at 808 / 858 / 808 tokens (53–54s each). The full-rebuild
|
||||
figure (~34,600) is an estimate for package-change deploys — treat it as the upper
|
||||
bound.
|
||||
|
||||
zdeploy pyapp -Note "Fix nav alignment"
|
||||
zdeploy edge # reload edge nginx config
|
||||
zdeploy all -Note "weekly release"
|
||||
|
||||
---
|
||||
|
||||
zstart_docker — Start local Docker stack
|
||||
----------------------------------------
|
||||
Not measured (est. ~500–1,500 tokens/run) | typical 1 run/day
|
||||
|
||||
One-time setup per session; doesn't need agent involvement.
|
||||
|
||||
---
|
||||
|
||||
Backup & Sync
|
||||
-------------
|
||||
|
||||
zbackup — Backup projects locally
|
||||
---------------------------------
|
||||
Measured: ~436 tokens/run | est. raw orchestration: ~1,200–2,500 | typical 1–2 runs/day
|
||||
|
||||
Raw, Claude enumerates files, decides exclusions, compresses, and stamps timestamps.
|
||||
You decide when to snapshot.
|
||||
|
||||
zbackup # everything + scripts folder
|
||||
zbackup pyapp -Tag "pre-refactor"
|
||||
|
||||
---
|
||||
|
||||
zsync — Sync backups offsite
|
||||
----------------------------
|
||||
Measured: ~769 tokens/run | est. raw orchestration: ~1,500–3,000 | typical 1 run/day
|
||||
|
||||
Raw, Claude tracks file diffs, runs robocopy, and verifies the copy. You manage
|
||||
cadence independently.
|
||||
|
||||
zsync
|
||||
zsync viteapp # build + mirror dist to $env:ZSYNC_DEST
|
||||
|
||||
---
|
||||
|
||||
zbackup_ec2 — Pull backups from the server
|
||||
------------------------------------------
|
||||
Measured: ~334 tokens/run | est. raw orchestration: ~1,000–2,000 | typical 1 run/day
|
||||
|
||||
Separates database/app backup from code changes. Claude focuses on code; you manage
|
||||
infrastructure snapshots.
|
||||
|
||||
zbackup_ec2
|
||||
|
||||
---
|
||||
|
||||
Diagnostics & Troubleshooting
|
||||
-----------------------------
|
||||
|
||||
zec2 — Check EC2 reachability
|
||||
-----------------------------
|
||||
Measured: ~331 tokens/run (zec2online: ~267) | est. raw orchestration: ~1,000–2,000 | typical 5–8 runs/day
|
||||
|
||||
When a deploy fails you run this first to confirm EC2 is reachable and the right
|
||||
build is live — before asking Claude to debug. Raw, that's blind network diagnostics
|
||||
over SSH. Runs frequently alongside zdeploy.
|
||||
|
||||
zec2 viteapp
|
||||
zec2 # check all projects
|
||||
zec2online sp # lightweight HTTP-only variant
|
||||
|
||||
---
|
||||
|
||||
zrepair — Audit & repair container routing
|
||||
------------------------------------------
|
||||
Measured: ~364 tokens/run (clean audit) | est. raw orchestration: ~2,000–4,000 | typical 1–2 runs/day
|
||||
|
||||
When a page 502s, this isolates routing vs. DNS vs. app logic across several
|
||||
containers — rather than handing Claude an SSH session to figure out blind. The
|
||||
364-token figure is a healthy run with nothing to repair; a run that actually
|
||||
restarts containers emits more. Raw, Claude would SSH per container and reason
|
||||
across each check.
|
||||
|
||||
zrepair viteapp
|
||||
|
||||
---
|
||||
|
||||
Daily Token Savings Summary
|
||||
---------------------------
|
||||
|
||||
Per-run × runs/day. The per-run figures are measured; the daily totals multiply
|
||||
them by assumed typical run counts (midpoints) — zdeploy and zrestart at
|
||||
10–15/day dominate the sum, so scale the total to your own cadence. The est. raw
|
||||
column approximates what Claude would burn orchestrating the same work with no
|
||||
scripts.
|
||||
|
||||
| Script | Measured/run | Runs/day | Measured/day | Est. raw/day |
|
||||
|--------|-------------:|:--------:|-------------:|-------------:|
|
||||
| zstart | 762 | 2–3 | ~1,900 | ~3,800–9,000 |
|
||||
| zkill | 377 | 2–3 | ~940 | ~2,500–6,000 |
|
||||
| zrestart | 724 | 10–15 | ~9,050 | ~31,000–68,000 |
|
||||
| zdeploy (cached) | ~810 | 10–15 | ~10,100 | ~62,000–180,000 |
|
||||
| zec2 (+online) | ~330 | 5–8 | ~2,200 | ~6,500–16,000 |
|
||||
| zbackup | 436 | 1–2 | ~650 | ~1,800–5,000 |
|
||||
| zsync | 769 | 1 | ~770 | ~1,500–3,000 |
|
||||
| zbackup_ec2 | 334 | 1 | ~330 | ~1,000–2,000 |
|
||||
| zrepair | 364 | 1–2 | ~550 | ~3,000–6,000 |
|
||||
| Total (active dev day) | | | ~26,500 | ~115,000–295,000 est. |
|
||||
|
||||
The ~26,500/day figure is measured per-run at an assumed typical cadence —
|
||||
reproducible on the per-run side, workflow-specific on the multiplier. It reflects an
|
||||
active tool-development day of mostly cached deploys. The
|
||||
~115k–295k est. upper figure is what it would cost to have Claude drive the raw
|
||||
ssh/docker sequences instead — dominated by per-step reasoning on zdeploy and
|
||||
zrestart, not by output volume. Treat that column as an **upper bound, not a
|
||||
prediction**: a capable agent asked to deploy might well write its own wrapper
|
||||
script and ingest very little — the counterfactual depends entirely on how the
|
||||
agent chooses to work. A day with several full-rebuild deploys pushes the measured
|
||||
figure higher too, since each rebuild streams ~34,600 tokens.
|
||||
|
||||
Daily dollar savings during active tool development:
|
||||
|
||||
Script output the agent ingests is billed at input rates, so the measured column
|
||||
uses input pricing. The est.-raw column keeps the blended rate, because raw
|
||||
orchestration also generates agent output (reasoning and tool calls between steps).
|
||||
|
||||
| Model | Measured/day @ input rate | Est. raw/day @ blended rate |
|
||||
|-------|--------------------------:|----------------------------:|
|
||||
| Sonnet 5 | ~$0.08 ($3/1M) | ~$1.04–$2.66 ($9/1M) |
|
||||
| Opus 4.8 | ~$0.13 ($5/1M) | ~$1.73–$4.43 ($15/1M) |
|
||||
| Fable 5 | ~$0.27 ($10/1M) | ~$3.45–$8.85 ($30/1M) |
|
||||
|
||||
One-time ingest slightly understates the true cost: tokens that enter the context are
|
||||
re-sent on every later turn of the session (at cheaper cache-read rates when prompt
|
||||
caching applies), so the cumulative figure is somewhat higher than a single ingest.
|
||||
|
||||
Over a ~22-day working month, the measured savings run ~$2–$6/mo (Sonnet →
|
||||
Fable); the raw-orchestration estimate runs ~$23–$195/mo. The honest dollar
|
||||
figure is small — the real currency is context: every infrastructure token kept
|
||||
out of the window is context your agent keeps for the actual problem, and that's
|
||||
worth more than the dollars suggest.
|
||||
|
||||
---
|
||||
|
||||
Claude Model Token Costs (July 2026)
|
||||
------------------------------------
|
||||
|
||||
| Model | Input | Output | Typical use |
|
||||
|-------|-------|--------|-------------|
|
||||
| Haiku 4.5 | $1/1M | $5/1M | Quick edits, small changes |
|
||||
| Sonnet 5 | $3/1M | $15/1M | Daily coding, medium complexity |
|
||||
| Opus 4.8 | $5/1M | $25/1M | Complex reasoning, multi-file refactors |
|
||||
| Fable 5 | $10/1M | $50/1M | Advanced reasoning, agentic workflows |
|
||||
|
||||
---
|
||||
|
||||
When to Run Scripts Yourself vs. Ask the Agent
|
||||
----------------------------------------------
|
||||
|
||||
Run yourself when:
|
||||
- ✅ You know exactly what action is needed
|
||||
- ✅ The script is deterministic (same input = same output)
|
||||
- ✅ You want to parallelize — run zstart while asking Claude for code
|
||||
- ✅ You're troubleshooting and need fast feedback loops
|
||||
|
||||
Ask the agent when:
|
||||
- ❌ You need conditional logic ("if this test fails, try X")
|
||||
- ❌ You're chaining operations that depend on each other's output
|
||||
- ❌ You want the agent to interpret script output and decide next steps
|
||||
|
||||
Bottom line: These scripts are optimized for you to run directly. Use them. Save
|
||||
tokens. Let Claude focus on coding.
|
||||
268
ZHelpers.ps1
268
ZHelpers.ps1
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly.
|
||||
|
||||
@ -105,6 +105,33 @@ function Get-ZEdgeProject {
|
||||
}
|
||||
|
||||
# Remote compose directory for a project: remote.composeDir if set, else remote.path.
|
||||
# How a docker stack gets its images: built here, or pulled from a registry.
|
||||
#
|
||||
# `docker compose pull` is right for a stack of published images and wrong for
|
||||
# one built from a Dockerfile in the tree - there is nothing to pull. The trap
|
||||
# is what happens next: `docker compose up -d` builds only when the image is
|
||||
# MISSING. So the FIRST deploy of a build-from-source stack works, and every
|
||||
# one after it uploads the new code, starts the old image, and reports success.
|
||||
# That is worse than an error, because nothing looks wrong: the deploy is
|
||||
# green, the container is up, and the change simply is not in it.
|
||||
#
|
||||
# deploy.build opts a project into building instead. --pull refreshes the base
|
||||
# image at the same time, so a rebuild also picks up its security updates
|
||||
# rather than pinning whatever happened to be on the box the first time.
|
||||
function Get-DockerImageStep {
|
||||
param($Proj, [Parameter(Mandatory)][string]$RemotePath)
|
||||
if ($Proj -and $Proj.deploy -and $Proj.deploy.build) {
|
||||
return @{
|
||||
Label = 'docker compose build'
|
||||
Command = "cd $RemotePath && sudo docker compose build --pull"
|
||||
}
|
||||
}
|
||||
return @{
|
||||
Label = 'docker compose pull'
|
||||
Command = "cd $RemotePath && sudo docker compose pull"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RemoteComposeDir {
|
||||
param([Parameter(Mandatory)][string]$Key)
|
||||
$proj = Get-ZProject -Key $Key
|
||||
@ -279,10 +306,9 @@ function Invoke-DeployGitPull {
|
||||
# unreviewed SOURCE shipping; a stamp the script just wrote is not
|
||||
# that. It is still committed separately, one bump per release,
|
||||
# per the versioning rule - this only stops it being a gate.
|
||||
$deployWritten = @('build-version.json', 'CHANGELOG.md')
|
||||
$dirty = $dirty | Where-Object {
|
||||
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
$deployWritten -notcontains $path
|
||||
(Get-DeployStampFiles) -notcontains $path
|
||||
}
|
||||
if ($dirty) {
|
||||
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
||||
@ -315,6 +341,179 @@ function Invoke-DeployGitPull {
|
||||
}
|
||||
}
|
||||
|
||||
# ── Files the deploy itself writes ──────────────────────────────────────────
|
||||
#
|
||||
# zdeploy stamps the bumped build version, and appends a changelog line, into
|
||||
# the working tree after a successful run. Neither is unreviewed SOURCE, so
|
||||
# neither should make a tree look dirty to the guards below - leaving them in
|
||||
# scope made each deploy block the next one, over a change the operator never
|
||||
# made.
|
||||
#
|
||||
# One list, because two copies drift: the first copy knew about CHANGELOG.md
|
||||
# and not build_changelog.md, which is the name a project's own changelog
|
||||
# tool may write.
|
||||
function Get-DeployStampFiles {
|
||||
return @('build-version.json', 'CHANGELOG.md', 'build_changelog.md')
|
||||
}
|
||||
|
||||
# Tracked modifications, minus those stamps. Runs in the CURRENT directory;
|
||||
# both callers are inside a Push-Location on the project root.
|
||||
function Get-TrackedChangesExcludingStamps {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$dirty = git status --porcelain --untracked-files=no
|
||||
$stamps = Get-DeployStampFiles
|
||||
return @($dirty | Where-Object {
|
||||
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
$stamps -notcontains $path
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
# ── The release tag zdeploy owes the versioning scheme ──────────────────────
|
||||
#
|
||||
# The scheme says every release lays an annotated tag alongside its version
|
||||
# stamp. For a project whose build number lives OUTSIDE git nothing enforced
|
||||
# that, and the tag history quietly stopped tracking reality: one project kept
|
||||
# its number in a database and reached thirty-eight builds with four tags.
|
||||
# Those builds were not recoverable - the number only ever existed in the
|
||||
# database - so this stops the bleeding rather than back-filling.
|
||||
#
|
||||
# Opt-in per project, via deploy.tagOnDeploy. A project that already tags its
|
||||
# releases through a pull request must NOT also get a tag per deploy: a
|
||||
# git-side release ledger and a container's own deploy counter are two
|
||||
# different numbers on purpose.
|
||||
#
|
||||
# Runs only after the live build has been VERIFIED, and never throws. A deploy
|
||||
# that reached production must not be reported as failed because a tag could
|
||||
# not be written afterwards.
|
||||
function New-DeployTag {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Proj,
|
||||
[Parameter(Mandatory)][string]$Version,
|
||||
[string]$Note = "Build deployed"
|
||||
)
|
||||
if (-not ($Proj.deploy -and $Proj.deploy.tagOnDeploy)) { return }
|
||||
$root = $Proj.localRoot
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $root ".git"))) {
|
||||
Write-Host " tagOnDeploy is set but '$root' is not a git repo - no tag written." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "`n--- [6] Tagging the deployed commit as $Version ---" -ForegroundColor Cyan
|
||||
Push-Location -LiteralPath $root
|
||||
try {
|
||||
# The same PS 5.1 trap the rest of this file documents: success is
|
||||
# judged by $LASTEXITCODE, and git writes ordinary progress to stderr.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$sha = git rev-parse HEAD
|
||||
if ($LASTEXITCODE -ne 0 -or -not $sha) {
|
||||
Write-Host " Could not read HEAD - no tag written. The deploy stands." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
$sha = "$sha".Trim()
|
||||
$short = $sha.Substring(0, 7)
|
||||
|
||||
# The zip is taken from the WORKING TREE, so uncommitted changes ship
|
||||
# while the commit this tag names does not contain them. Say so rather
|
||||
# than let a tag quietly claim to describe the build.
|
||||
$dirty = Get-TrackedChangesExcludingStamps
|
||||
if ($dirty.Count -gt 0) {
|
||||
Write-Host " Working tree has $($dirty.Count) uncommitted change(s): $Version names $short, which is NOT everything that shipped." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
git rev-parse -q --verify "refs/tags/$Version" *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host " Tag $Version already exists - left alone." -ForegroundColor DarkYellow
|
||||
return
|
||||
}
|
||||
|
||||
git tag -a $Version -m "$Version - $Note"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " git tag failed - the deploy stands, the tag does not." -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
git push origin $Version
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " $Version written locally but the push failed. Push it when you can: git push origin $Version" -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
Write-Host " Tagged $Version at $short and pushed." -ForegroundColor Green
|
||||
}
|
||||
catch {
|
||||
Write-Host " Tagging failed ($($_.Exception.Message)) - the deploy stands." -ForegroundColor Yellow
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ── zstart's pull: fast-forward if you can, start regardless ─────────────────
|
||||
#
|
||||
# The OPPOSITE failure mode from Invoke-DeployGitPull above, on purpose. A
|
||||
# deploy that cannot prove it has the default branch must refuse - shipping
|
||||
# stale code and still bumping the build is a silent lie. A dev server has no
|
||||
# such stake: the person is already at a checkout they chose, often a feature
|
||||
# branch, and a pull that cannot complete is information, not a reason to
|
||||
# leave them without a server. So this never throws, never switches branch,
|
||||
# and never touches a dirty tree - it reports, and zstart carries on.
|
||||
#
|
||||
# It shares the deploy helper's one hard-won mechanic. Under zstart's
|
||||
# $ErrorActionPreference = 'Stop', the previous inline `git pull --ff-only
|
||||
# 2>&1` wrapped every stderr line in a terminating ErrorRecord - and git
|
||||
# prints ordinary fetch progress ("From https://...") on stderr. A pull that
|
||||
# SUCCEEDED aborted the start before its own skip-and-continue branch could
|
||||
# run (#130). Success is judged by $LASTEXITCODE, nothing is redirected, and
|
||||
# the preference drops to Continue for this function's scope only.
|
||||
#
|
||||
# Fetch, then merge --ff-only against the branch's upstream, rather than
|
||||
# `git pull` - see the FETCH_HEAD race note on Invoke-DeployGitPull.
|
||||
function Invoke-StartGitPull {
|
||||
param([Parameter(Mandatory)][string]$Root)
|
||||
$result = [pscustomobject]@{ Ok = $false; Skipped = $false; Message = '' }
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Root '.git'))) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = "'$Root' is not a git repo"
|
||||
return $result
|
||||
}
|
||||
Push-Location -LiteralPath $Root
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||
# instead of blocking the server start on a "Username for ..." prompt.
|
||||
$prevPrompt = $env:GIT_TERMINAL_PROMPT
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git fetch origin --prune
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = 'git fetch failed - credentials, or the remote'
|
||||
return $result
|
||||
}
|
||||
$branch = git rev-parse --abbrev-ref HEAD
|
||||
$upstream = git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not $upstream) {
|
||||
$result.Skipped = $true
|
||||
$result.Message = "'$branch' has no upstream to fast-forward from"
|
||||
return $result
|
||||
}
|
||||
$before = git rev-parse HEAD
|
||||
git merge --ff-only $upstream
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$result.Message = "cannot fast-forward '$branch' onto $upstream (diverged, or local changes in the way) - left as is"
|
||||
return $result
|
||||
}
|
||||
$result.Ok = $true
|
||||
$result.Message = if ((git rev-parse HEAD) -eq $before) { 'Already up to date.' }
|
||||
else { "Now at: $(git log -1 --oneline)" }
|
||||
return $result
|
||||
}
|
||||
finally {
|
||||
$env:GIT_TERMINAL_PROMPT = $prevPrompt
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
# ── Build-version helpers ────────────────────────────────────────────────────
|
||||
|
||||
function Read-JsonBuildVersion {
|
||||
@ -334,18 +533,18 @@ function Get-ServerSideVersionCommand {
|
||||
the public edge: zdeploy's post-deploy check, zec2, zec2online, and
|
||||
bash/zhelpers.sh. That works only for as long as the endpoint is
|
||||
public, and it should not be: www's /build-version.json has been
|
||||
blocked at the edge since the 2026-05-29 security pass, and evo.ehs
|
||||
blocked at the edge since an earlier security pass, and one app
|
||||
answering /api/build-version to anyone is the inconsistency this
|
||||
closes.
|
||||
|
||||
Going through the edge is also how a check reads the WRONG product.
|
||||
The proxy answers from whichever vhost matches the Host header, so a
|
||||
service with no public route gets somebody else's version back --
|
||||
evo-ai's deploy check compared evo.ehs's build against its own and
|
||||
reported a failure on a deploy that had worked (evo.scripts#101).
|
||||
one project's deploy check compared another project's build against
|
||||
its own and reported a failure on a deploy that had worked.
|
||||
|
||||
A project reached only on the shared docker network cannot be curled
|
||||
from the host: evoehs_app publishes no port. It IS reachable by name
|
||||
from the host: an app container that publishes no port. It IS reachable by name
|
||||
from another container on that network, which also exercises the real
|
||||
HTTP path -- so this proves the app is serving, not merely that its
|
||||
database knows a version.
|
||||
@ -355,7 +554,7 @@ function Get-ServerSideVersionCommand {
|
||||
"verify": {
|
||||
"path": "/api/build-version",
|
||||
"viaProxy": "evo_edge_proxy",
|
||||
"upstream": "evoehs_app:80"
|
||||
"upstream": "myapp_app:80"
|
||||
}
|
||||
|
||||
Returns $null when either key is missing, so every project without
|
||||
@ -376,8 +575,8 @@ function Get-LabelFromVersionJson {
|
||||
The build label out of a version endpoint's JSON text, or $null.
|
||||
|
||||
.DESCRIPTION
|
||||
Two field names in the fleet: evo.ehs answers `build_version` on
|
||||
/api/build-version, evo-ai answers `version` on /health. Both mean
|
||||
Two field names in the fleet: one app answers `build_version` on
|
||||
/api/build-version, another answers `version` on /health. Both mean
|
||||
"the build that is live", so both are accepted rather than making an
|
||||
app rename its own field.
|
||||
#>
|
||||
@ -391,14 +590,50 @@ function Get-LabelFromVersionJson {
|
||||
}
|
||||
|
||||
function Get-LabelFromBuildJsonObj {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
The build label out of a parsed build-version.json, or $null.
|
||||
|
||||
.DESCRIPTION
|
||||
Three stamp shapes, and this has to read all of them because deploy
|
||||
verification runs BOTH sides through it - the local stamp and the one read
|
||||
back from the running container. A shape it cannot read does not fail
|
||||
loudly; it collapses both sides to the same wrong string and the comparison
|
||||
passes unconditionally, which is worse than having no check at all.
|
||||
|
||||
{ "major":1, "rc":0, "beta":0, "alpha":0, "build":98 } object form
|
||||
{ "version": "v1.0.0.0.98" } string form
|
||||
{ "productVersion": "1.2", "buildNumber": 7 } legacy form
|
||||
|
||||
The string form is what the versioning scheme specifies and what zbump
|
||||
writes, so it is checked FIRST - a stamp carrying both an explicit version
|
||||
and stray numeric fields means the version it states.
|
||||
|
||||
Earned the hard way: the string form used to fall through to the legacy
|
||||
branch, where productVersion is null ("") and buildNumber is null (0), so
|
||||
EVERY string-form stamp became "v.0". A site verified `expect v.0` against
|
||||
a live `v.0` and reported PASS while serving whatever it liked.
|
||||
#>
|
||||
param($obj)
|
||||
if (-not $obj) { return $null }
|
||||
# Five-segment scheme: v{major}.{rc}.{beta}.{alpha}.{build}
|
||||
|
||||
# String form: the version is stated, so state it back. Trimmed, and given
|
||||
# the leading v the other branches add, so all three shapes are comparable.
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$obj.version)) {
|
||||
$v = ([string]$obj.version).Trim()
|
||||
return $(if ($v -match '^[vV]') { 'v' + $v.Substring(1) } else { "v$v" })
|
||||
}
|
||||
|
||||
# Object form: v{major}.{rc}.{beta}.{alpha}.{build}
|
||||
if ($null -ne $obj.build -or $null -ne $obj.alpha) {
|
||||
$alpha = if ($null -ne $obj.alpha) { [int]$obj.alpha } else { 1 }
|
||||
return "v$([int]$obj.major).$([int]$obj.rc).$([int]$obj.beta).$alpha.$([int]$obj.build)"
|
||||
}
|
||||
|
||||
# Legacy two-part stamp (projects not yet migrated): v{productVersion}.{buildNumber}
|
||||
# Only reached when there is something to build it from; otherwise $null, so
|
||||
# a caller sees "no label" instead of a label that matches everything.
|
||||
if ([string]::IsNullOrWhiteSpace([string]$obj.productVersion) -and $null -eq $obj.buildNumber) { return $null }
|
||||
return "v$([string]$obj.productVersion).$([int]$obj.buildNumber)"
|
||||
}
|
||||
|
||||
@ -852,8 +1087,7 @@ function Get-VerifyTimeout {
|
||||
BOOT say so, instead of every deploy of it warning on a success. An
|
||||
app that runs database migrations in its entrypoint exceeds a 30s
|
||||
window on every deploy that ships one - and a warning that fires on
|
||||
routine success trains people to ignore the one that matters
|
||||
(evo.scripts#101).
|
||||
routine success trains people to ignore the one that matters.
|
||||
#>
|
||||
param($Proj, [int]$DefaultSec)
|
||||
if ($Proj.verify -and $Proj.verify.timeoutSeconds) {
|
||||
@ -870,7 +1104,7 @@ function Get-VerifyAttempts {
|
||||
without ssh.
|
||||
|
||||
.DESCRIPTION
|
||||
Three channels exist, and their order is the whole point (#101):
|
||||
Three channels exist, and their order is the whole point:
|
||||
|
||||
exec - docker-network read via verify.viaProxy/upstream. Cannot
|
||||
answer from the wrong product, works for apps with no
|
||||
@ -880,8 +1114,8 @@ function Get-VerifyAttempts {
|
||||
edge - http://<ip> with a Host header. The proxy answers from
|
||||
whichever vhost MATCHES that header, so without one this
|
||||
channel can only reach the default vhost - which is a
|
||||
different product (that is how evo-ai's check once read
|
||||
evo.ehs's build number). It is therefore included ONLY
|
||||
different product (that is how one project's check once read
|
||||
another's build number). It is therefore included ONLY
|
||||
when the project has a host to route by, and never
|
||||
otherwise: no answer at all beats somebody else's answer.
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# ZKillOnly.ps1 — stop local dev server listeners for any project in zconfig.json.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json.
|
||||
#
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "v1.0.0.0.23"
|
||||
"version": "v1.0.0.0.28"
|
||||
}
|
||||
|
||||
BIN
releases/zscripts-v1.0.0.0.24.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.24.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.24.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.24.zip.sha256
Normal file
@ -0,0 +1 @@
|
||||
250f68fe5fd46b95e3c393c19352aff1e7d2afc16cff2d76efb2487f1c8ba6df zscripts-v1.0.0.0.24.zip
|
||||
BIN
releases/zscripts-v1.0.0.0.25.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.25.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.25.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.25.zip.sha256
Normal file
@ -0,0 +1 @@
|
||||
6f6f1bf1b46e014e0518ef4ffa2b64e455e894681d5b337ffdd947b0efd0d538 zscripts-v1.0.0.0.25.zip
|
||||
BIN
releases/zscripts-v1.0.0.0.26.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.26.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.26.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.26.zip.sha256
Normal file
@ -0,0 +1 @@
|
||||
dfaa85f53e4dd16789ac0f1f8c9d7e506b56c0c068f4c5f8e2c4d1cce7db8d95 zscripts-v1.0.0.0.26.zip
|
||||
BIN
releases/zscripts-v1.0.0.0.27.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.27.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.27.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.27.zip.sha256
Normal file
@ -0,0 +1 @@
|
||||
f5b940344ffd832032b0c62e65e77f94bc2de294e690c9fd68362deb99d21e82 zscripts-v1.0.0.0.27.zip
|
||||
BIN
releases/zscripts-v1.0.0.0.28.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.28.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.28.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.28.zip.sha256
Normal file
@ -0,0 +1 @@
|
||||
5eca5198ba500bb0e1ceb1e5000cfb405d5fb5024fa500daa8f64f9c012c1291 zscripts-v1.0.0.0.28.zip
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -28,11 +28,20 @@ from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Every markdown file that owes the repo a plain-text twin.
|
||||
PAIRS = (
|
||||
("README.md", "README.txt"),
|
||||
("CHANGELOG.md", "CHANGELOG.txt"),
|
||||
)
|
||||
def pairs() -> tuple[tuple[str, str], ...]:
|
||||
"""Every root-level markdown file, with the twin it owes.
|
||||
|
||||
Discovered rather than listed. The list was hand-kept, and two files had
|
||||
quietly outgrown it - ELEVATOR_PITCH.md and TOKEN_SAVINGS.md had no twin
|
||||
at all, because adding a document and remembering to add it here are two
|
||||
separate acts and the second one is the one that gets skipped. Discovery
|
||||
makes them one act.
|
||||
"""
|
||||
return tuple((f.name, f.with_suffix(".txt").name) for f in sorted(ROOT.glob("*.md")))
|
||||
|
||||
|
||||
#: Kept as a name because the Pester suite reads it to know what to check.
|
||||
PAIRS = pairs()
|
||||
|
||||
|
||||
def _inline(text: str) -> str:
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# setup_backup_schedule.ps1 — create a scheduled task for daily backups + OneDrive sync
|
||||
#
|
||||
|
||||
108
site/index.html
Normal file
108
site/index.html
Normal file
@ -0,0 +1,108 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>zscripts — one-word dev commands</title>
|
||||
<meta name="description" content="One-word commands for AI-assisted development. Routine start, deploy, and backup chores become scripts — saving tokens and keeping the AI's context window focused on real work.">
|
||||
<link rel="canonical" href="https://zscripts.evomedia.net/">
|
||||
|
||||
<!-- Open Graph — LinkedIn, Slack and the rest build link previews from
|
||||
these. The URLs must be absolute: a relative og:image is dropped by the
|
||||
strict crawlers and the card renders as bare text. -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://zscripts.evomedia.net/">
|
||||
<meta property="og:site_name" content="evomedia.net">
|
||||
<meta property="og:title" content="zscripts — one-word dev commands">
|
||||
<meta property="og:description" content="One-word commands for AI-assisted development. Routine start, deploy and backup chores become scripts, keeping the AI's context window on real work.">
|
||||
<meta property="og:image" content="https://zscripts.evomedia.net/og-card.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="630">
|
||||
<meta property="og:image:alt" content="zscripts — one-word dev commands, over a terminal showing zdeploy, zbackup and zrestart.">
|
||||
|
||||
<!-- Twitter/X card, reusing the same image. -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="zscripts — one-word dev commands">
|
||||
<meta name="twitter:description" content="One-word commands for AI-assisted development. Routine start, deploy and backup chores become scripts, keeping the AI's context window on real work.">
|
||||
<meta name="twitter:image" content="https://zscripts.evomedia.net/og-card.png">
|
||||
<style>
|
||||
:root{
|
||||
--bg:#0d1117; --panel:#161b22; --border:#2a313c;
|
||||
--fg:#e6edf3; --muted:#9aa7b4; --accent:#4ea1ff; --accent2:#3fb950;
|
||||
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
|
||||
--sans:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{margin:0;padding:0}
|
||||
body{background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.55;
|
||||
-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||||
.wrap{max-width:760px;margin:0 auto;padding:64px 24px 80px}
|
||||
.eyebrow{font-family:var(--mono);font-size:.8rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
|
||||
h1{font-size:2.6rem;line-height:1.1;margin:.4rem 0 .2rem;font-weight:700}
|
||||
h1 .z{color:var(--accent)}
|
||||
.tag{font-size:1.2rem;color:var(--muted);margin:0 0 2rem}
|
||||
.lead{font-size:1.05rem;color:var(--fg);margin:0 0 2rem}
|
||||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:20px 22px;margin:0 0 18px}
|
||||
.card h2{font-size:.95rem;margin:0 0 12px;color:var(--muted);font-weight:600;letter-spacing:.02em;text-transform:uppercase}
|
||||
pre{margin:0;font-family:var(--mono);font-size:.92rem;overflow-x:auto;color:var(--fg)}
|
||||
pre .c{color:var(--muted)}
|
||||
pre .g{color:var(--accent2)}
|
||||
.cmds{display:grid;grid-template-columns:auto 1fr;gap:6px 18px;font-size:.95rem}
|
||||
.cmds code{font-family:var(--mono);color:var(--accent);white-space:nowrap}
|
||||
.cmds span{color:var(--muted)}
|
||||
.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:6px}
|
||||
a.btn{display:inline-block;text-decoration:none;font-weight:600;font-size:.95rem;
|
||||
padding:11px 20px;border-radius:9px;border:1px solid var(--border)}
|
||||
a.primary{background:var(--accent);color:#04121f;border-color:var(--accent)}
|
||||
a.ghost{color:var(--fg)}
|
||||
a.primary:hover{filter:brightness(1.08)}
|
||||
a.ghost:hover{border-color:var(--accent);color:var(--accent)}
|
||||
footer{margin-top:44px;padding-top:20px;border-top:1px solid var(--border);color:var(--muted);font-size:.85rem}
|
||||
footer a{color:var(--muted)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="wrap">
|
||||
<p class="eyebrow">evomedia.net · developer tools</p>
|
||||
<h1><span class="z">z</span>scripts</h1>
|
||||
<p class="tag">Short, one-word commands for multi-project development.</p>
|
||||
|
||||
<p class="lead">
|
||||
A small suite of PowerShell commands built for AI-assisted
|
||||
development. Routine start / deploy / backup chores become single
|
||||
words a coding agent can run without reasoning through them — which
|
||||
saves tokens, but the real win is keeping the AI's context window
|
||||
focused on the actual work instead of housekeeping.
|
||||
</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>The idea</h2>
|
||||
<pre><span class="c"># every chore the agent doesn't narrate is context kept free</span>
|
||||
<span class="g">zstart</span> sp <span class="c"># launch a project's dev server</span>
|
||||
<span class="g">zdeploy</span> sp <span class="c"># package + ship it</span>
|
||||
<span class="g">zbackup</span> all <span class="c"># snapshot the databases</span></pre>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>What's in the box</h2>
|
||||
<div class="cmds">
|
||||
<code>zstart</code><span>run a project's dev server locally</span>
|
||||
<code>zdeploy</code><span>build and deploy to the server</span>
|
||||
<code>zbackup</code><span>back up project databases</span>
|
||||
<code>zrestart</code><span>restart a running dev server</span>
|
||||
<code>zkill</code><span>stop a dev server cleanly</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn primary" href="https://github.com/evomedia-net/evo.zscripts">View on GitHub</a>
|
||||
<a class="btn ghost" href="https://github.com/evomedia-net/evo.zscripts#readme">Read the docs</a>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
An <a href="https://evomedia.net">evomedia.net</a> project ·
|
||||
open source, sanitized for public use.
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
BIN
site/og-card.png
Normal file
BIN
site/og-card.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
7
site/robots.txt
Normal file
7
site/robots.txt
Normal file
@ -0,0 +1,7 @@
|
||||
# zscripts.evomedia.net — the public page for this toolkit.
|
||||
#
|
||||
# Deliberately the opposite of the candidate pages on this estate
|
||||
# (cardiff, opensesame, kelly, unify), which disallow everything. This one
|
||||
# is an open-source project page: being found is the point.
|
||||
User-agent: *
|
||||
Allow: /
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels - dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -73,14 +73,30 @@ BeforeAll {
|
||||
$fixture | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $script:Install "zconfig.json") -Encoding UTF8
|
||||
|
||||
# Run a script in a child process; capture merged output and exit code.
|
||||
#
|
||||
# Memoised on the exact command. Several checks deliberately assert
|
||||
# different things about the SAME invocation - that running bare exits
|
||||
# non-zero, that its usage lists the project keys, and that the usage
|
||||
# never mentions the underscore comment key are three checks of one run.
|
||||
# Starting a Windows PowerShell costs about 1.7 s, so re-running the same
|
||||
# command to ask it a second question is the single most expensive thing
|
||||
# this file does. The scripts reached here either refuse their input or
|
||||
# inspect an unused fixture port, so none of them has a side effect a
|
||||
# second run would reveal.
|
||||
$script:zRuns = @{}
|
||||
|
||||
function Invoke-ZScript {
|
||||
param([string]$Script, [string[]]$ScriptArgs = @())
|
||||
$key = @($Script) + $ScriptArgs -join "`n"
|
||||
if ($script:zRuns.ContainsKey($key)) { return $script:zRuns[$key] }
|
||||
$path = Join-Path $script:Install $Script
|
||||
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" }
|
||||
return [pscustomobject]@{
|
||||
$result = [pscustomobject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($out -join "`n")
|
||||
}
|
||||
$script:zRuns[$key] = $result
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels - dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
197
tests/DeployTag.Tests.ps1
Normal file
197
tests/DeployTag.Tests.ps1
Normal file
@ -0,0 +1,197 @@
|
||||
# zdeploy lays the release tag it already knows the number for.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# A versioning scheme that asks every release to lay an annotated tag needs
|
||||
# something to enforce it. For a project whose build number lives OUTSIDE git -
|
||||
# in a database, say - nothing did, and one project reached thirty-eight builds
|
||||
# with four tags. Those builds were not recoverable: the number only ever
|
||||
# existed in the database.
|
||||
#
|
||||
# These tests drive REAL git against a real bare remote in a temp directory,
|
||||
# the way StartGitPull.Tests.ps1 does. A stand-in that faked git would prove
|
||||
# nothing about the two properties that matter most here: that the tag is
|
||||
# annotated and pushed, and that NOTHING this function does can fail a deploy
|
||||
# which already reached production.
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||
|
||||
$script:tmpRoots = New-Object System.Collections.ArrayList
|
||||
|
||||
function New-TempDir {
|
||||
param([string]$Tag)
|
||||
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zdeploy-tag-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
[void]$script:tmpRoots.Add($dir)
|
||||
return $dir
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
# Test plumbing only. The thing under test does its own git handling
|
||||
# and must not go through here.
|
||||
param([string]$In, [string[]]$GitArgs)
|
||||
Push-Location -LiteralPath $In
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
|
||||
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
|
||||
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
|
||||
return $out
|
||||
}
|
||||
finally { Pop-Location }
|
||||
}
|
||||
|
||||
function New-RepoWithRemote {
|
||||
param([string]$Tag)
|
||||
$remote = New-TempDir "$Tag-remote"
|
||||
Invoke-Git -In $remote -GitArgs @('init', '--bare', '-q') | Out-Null
|
||||
$work = New-TempDir "$Tag-work"
|
||||
Invoke-Git -In $work -GitArgs @('init', '-q', '-b', 'main') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('config', 'user.email', 'test@example.com') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('config', 'user.name', 'Test') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $work 'app.txt') -Value 'v1' -Encoding utf8
|
||||
Invoke-Git -In $work -GitArgs @('add', '-A') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('commit', '-q', '-m', 'first') | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('remote', 'add', 'origin', $remote) | Out-Null
|
||||
Invoke-Git -In $work -GitArgs @('push', '-q', '-u', 'origin', 'main') | Out-Null
|
||||
return @{ Work = $work; Remote = $remote }
|
||||
}
|
||||
|
||||
function New-Proj {
|
||||
param([string]$Root, $TagOnDeploy = $true)
|
||||
$deploy = if ($null -eq $TagOnDeploy) {
|
||||
[pscustomobject]@{ zipName = 'X.zip' }
|
||||
} else {
|
||||
[pscustomobject]@{ zipName = 'X.zip'; tagOnDeploy = $TagOnDeploy }
|
||||
}
|
||||
return [pscustomobject]@{ localRoot = $Root; deploy = $deploy }
|
||||
}
|
||||
|
||||
function Get-Tags {
|
||||
param([string]$In)
|
||||
$out = Invoke-Git -In $In -GitArgs @('tag', '--list')
|
||||
return @($out | Where-Object { $_ -and $_.ToString().Trim() } |
|
||||
ForEach-Object { $_.ToString().Trim() })
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
foreach ($d in $script:tmpRoots) {
|
||||
Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'New-DeployTag' {
|
||||
|
||||
It 'tags the deployed commit and pushes it' {
|
||||
$r = New-RepoWithRemote 'happy'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.39' -Note 'Build deployed'
|
||||
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.39'
|
||||
# On the remote too: a tag only in the local checkout is not a record.
|
||||
$remoteTags = Invoke-Git -In $r.Work -GitArgs @('ls-remote', '--tags', 'origin')
|
||||
($remoteTags -join "`n") | Should -Match 'refs/tags/v0\.0\.1\.0\.39'
|
||||
}
|
||||
|
||||
It 'writes an ANNOTATED tag carrying the version and the note' {
|
||||
$r = New-RepoWithRemote 'annotated'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v1.2.3.4.5' -Note 'Ask AI sources'
|
||||
|
||||
# cat-file says "tag" for an annotated object and "commit" for a
|
||||
# lightweight one. The scheme asks for annotated.
|
||||
$type = Invoke-Git -In $r.Work -GitArgs @('cat-file', '-t', 'v1.2.3.4.5')
|
||||
($type -join '').Trim() | Should -Be 'tag'
|
||||
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v1.2.3.4.5', '--format=%(contents)')
|
||||
($msg -join ' ') | Should -Match 'v1\.2\.3\.4\.5'
|
||||
($msg -join ' ') | Should -Match 'Ask AI sources'
|
||||
}
|
||||
|
||||
It 'points the tag at the commit that was deployed' {
|
||||
$r = New-RepoWithRemote 'sha'
|
||||
$head = (Invoke-Git -In $r.Work -GitArgs @('rev-parse', 'HEAD') -join '').Trim()
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v9.9.9.9.9'
|
||||
$tagged = (Invoke-Git -In $r.Work -GitArgs @('rev-list', '-n', '1', 'v9.9.9.9.9') -join '').Trim()
|
||||
$tagged | Should -Be $head
|
||||
}
|
||||
|
||||
It 'does nothing at all unless the project opts in' {
|
||||
$r = New-RepoWithRemote 'optout'
|
||||
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $false) -Version 'v0.0.0.0.1'
|
||||
Get-Tags -In $r.Work | Should -BeNullOrEmpty
|
||||
|
||||
# And when the key is absent entirely, which is every existing project.
|
||||
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $null) -Version 'v0.0.0.0.2'
|
||||
Get-Tags -In $r.Work | Should -BeNullOrEmpty
|
||||
}
|
||||
|
||||
It 'leaves an existing tag alone rather than failing a redeploy' {
|
||||
$r = New-RepoWithRemote 'exists'
|
||||
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'first'
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'second' } |
|
||||
Should -Not -Throw
|
||||
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v0.0.1.0.40', '--format=%(contents)')
|
||||
($msg -join ' ') | Should -Match 'first'
|
||||
($msg -join ' ') | Should -Not -Match 'second'
|
||||
}
|
||||
|
||||
It 'never throws when the directory is not a git repo' {
|
||||
$plain = New-TempDir 'notrepo'
|
||||
{ New-DeployTag -Proj (New-Proj $plain) -Version 'v0.0.0.0.3' } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It 'never throws when the push fails, and still writes the tag locally' {
|
||||
# A deploy that reached production must not be reported as failed
|
||||
# because a tag could not leave the machine.
|
||||
$r = New-RepoWithRemote 'badremote'
|
||||
Invoke-Git -In $r.Work -GitArgs @('remote', 'set-url', 'origin',
|
||||
(Join-Path ([IO.Path]::GetTempPath()) 'no-such-remote-zz')) | Out-Null
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.41' } | Should -Not -Throw
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.41'
|
||||
}
|
||||
|
||||
It 'survives the deploy-wide ErrorActionPreference of Stop' {
|
||||
# The trap this whole file documents: under 'Stop', PS 5.1 turns any
|
||||
# native stderr line into a terminating error. git push writes its
|
||||
# ordinary progress to stderr, so a tag that works interactively can
|
||||
# still kill a deploy.
|
||||
$r = New-RepoWithRemote 'stoppref'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.42' } | Should -Not -Throw
|
||||
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.42'
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-DeployStampFiles' {
|
||||
|
||||
It 'covers every changelog name the fleet actually writes' {
|
||||
# The old inline copy knew CHANGELOG.md and not build_changelog.md,
|
||||
# a name a project's own changelog tool may write - so that stamp
|
||||
# counted as unreviewed source in the branch guard.
|
||||
$stamps = Get-DeployStampFiles
|
||||
$stamps | Should -Contain 'build-version.json'
|
||||
$stamps | Should -Contain 'CHANGELOG.md'
|
||||
$stamps | Should -Contain 'build_changelog.md'
|
||||
}
|
||||
|
||||
It 'does not count a stamp the deploy just wrote as an uncommitted change' {
|
||||
$r = New-RepoWithRemote 'stamps'
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'x' -Encoding utf8
|
||||
Invoke-Git -In $r.Work -GitArgs @('add', '-A') | Out-Null
|
||||
Invoke-Git -In $r.Work -GitArgs @('commit', '-q', '-m', 'add changelog') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'stamped by the deploy' -Encoding utf8
|
||||
|
||||
Push-Location -LiteralPath $r.Work
|
||||
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
|
||||
$changes.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'still reports real source changes' {
|
||||
$r = New-RepoWithRemote 'realchange'
|
||||
Set-Content -LiteralPath (Join-Path $r.Work 'app.txt') -Value 'edited' -Encoding utf8
|
||||
Push-Location -LiteralPath $r.Work
|
||||
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
|
||||
$changes.Count | Should -Be 1
|
||||
($changes -join ' ') | Should -Match 'app\.txt'
|
||||
}
|
||||
}
|
||||
82
tests/DockerImageStep.Tests.ps1
Normal file
82
tests/DockerImageStep.Tests.ps1
Normal file
@ -0,0 +1,82 @@
|
||||
# How a docker stack gets its images, and the deploy that shipped nothing.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# `docker compose pull` is right for a stack of published images - Prometheus,
|
||||
# Grafana, docker-mailserver - and wrong for one built from a Dockerfile in the
|
||||
# tree, where there is nothing to pull.
|
||||
#
|
||||
# The trap is what happens after. `docker compose up -d` builds only when the
|
||||
# image is MISSING, so the first deploy of a build-from-source stack works and
|
||||
# every one after it uploads the new code, starts the OLD image, and reports
|
||||
# success. Green deploy, healthy container, and the change is not in it. That
|
||||
# is the failure this helper exists to prevent, and it is worse than an error
|
||||
# because nothing about it looks wrong.
|
||||
#
|
||||
# ZHelpers.ps1 is dot-sourced rather than zdeploy.ps1: zdeploy executes its
|
||||
# main flow on load, helpers only define functions.
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||
|
||||
function New-Proj { param($Build)
|
||||
if ($null -eq $Build) { return [pscustomobject]@{ deploy = [pscustomobject]@{ gitPull = $true } } }
|
||||
return [pscustomobject]@{ deploy = [pscustomobject]@{ build = $Build } }
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'Get-DockerImageStep - build here, or pull from a registry' {
|
||||
|
||||
It 'pulls by default, so every existing docker stack is unaffected' {
|
||||
$step = Get-DockerImageStep -Proj (New-Proj $null) -RemotePath '/home/u/stack/x'
|
||||
$step.Command | Should -BeLike '*docker compose pull*'
|
||||
$step.Command | Should -Not -BeLike '*build*'
|
||||
}
|
||||
|
||||
It 'pulls for a project with no deploy block at all' {
|
||||
$step = Get-DockerImageStep -Proj ([pscustomobject]@{}) -RemotePath '/home/u/stack/x'
|
||||
$step.Command | Should -BeLike '*docker compose pull*'
|
||||
}
|
||||
|
||||
It 'builds when the project asks to be built' {
|
||||
$step = Get-DockerImageStep -Proj (New-Proj $true) -RemotePath '/home/u/stack/x'
|
||||
$step.Command | Should -BeLike '*docker compose build*'
|
||||
$step.Command | Should -Not -BeLike '*compose pull*'
|
||||
}
|
||||
|
||||
It 'still pulls when build is explicitly false' {
|
||||
$step = Get-DockerImageStep -Proj (New-Proj $false) -RemotePath '/home/u/stack/x'
|
||||
$step.Command | Should -BeLike '*docker compose pull*'
|
||||
}
|
||||
|
||||
It 'refreshes the base image on a build, so a rebuild is not pinned to the first one' {
|
||||
$step = Get-DockerImageStep -Proj (New-Proj $true) -RemotePath '/home/u/stack/x'
|
||||
$step.Command | Should -BeLike '*--pull*'
|
||||
}
|
||||
|
||||
It 'runs in the project directory: <Build>' -ForEach @(
|
||||
@{ Build = $true }
|
||||
@{ Build = $false }
|
||||
) {
|
||||
$step = Get-DockerImageStep -Proj (New-Proj $Build) -RemotePath '/home/u/stack/ablecamera'
|
||||
$step.Command | Should -BeLike 'cd /home/u/stack/ablecamera &&*'
|
||||
}
|
||||
|
||||
It 'labels the step with what it actually does: <Build>' -ForEach @(
|
||||
@{ Build = $true; Expected = 'docker compose build' }
|
||||
@{ Build = $false; Expected = 'docker compose pull' }
|
||||
) {
|
||||
(Get-DockerImageStep -Proj (New-Proj $Build) -RemotePath '/x').Label | Should -Be $Expected
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'the docker deploy uses it' {
|
||||
|
||||
It 'no longer hardcodes compose pull' {
|
||||
$text = Get-Content -Raw (Join-Path (Split-Path -Parent $PSScriptRoot) "zdeploy.ps1")
|
||||
$body = $text.Substring($text.IndexOf('function Invoke-DockerDeploy'))
|
||||
$body = $body.Substring(0, $body.IndexOf('function Invoke-ZTokensPublish'))
|
||||
$body | Should -Match 'Get-DockerImageStep'
|
||||
$body | Should -Not -Match '"docker compose pull"'
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
90
tests/LinkPreview.Tests.ps1
Normal file
90
tests/LinkPreview.Tests.ps1
Normal file
@ -0,0 +1,90 @@
|
||||
# evomedia.net Token Savers - https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels - dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
# LinkPreview.Tests.ps1 - the public page renders a card, not a bare URL.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# Pasting zscripts.evomedia.net into LinkedIn, Slack or a message builds a card
|
||||
# from the page's og:* tags. The page had a title and a description and nothing
|
||||
# else, so it pasted as a bare URL.
|
||||
#
|
||||
# None of that is visible from here. The page is correct, the site is up, and
|
||||
# the only symptom is a card somewhere else - which is why the rule is asserted
|
||||
# rather than remembered.
|
||||
#
|
||||
# The last test is the one that earned its place: the first draft of the card
|
||||
# showed `ztests`, which is not a command in this repo. A card is public copy,
|
||||
# and public copy must not advertise a script that does not exist.
|
||||
|
||||
BeforeAll {
|
||||
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$script:Page = Join-Path $script:RepoRoot "site\index.html"
|
||||
$script:Card = Join-Path $script:RepoRoot "site\og-card.png"
|
||||
$script:Html = [IO.File]::ReadAllText($script:Page)
|
||||
|
||||
function Get-Meta {
|
||||
param([string]$Key)
|
||||
$pattern = '<meta (?:property|name)="' + [regex]::Escape($Key) + '" content="([^"]*)">'
|
||||
$m = [regex]::Match($script:Html, $pattern)
|
||||
if ($m.Success) { return $m.Groups[1].Value }
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
Describe "zscripts.evomedia.net link preview" {
|
||||
|
||||
It "carries the tags a card is built from" {
|
||||
foreach ($key in @('og:type', 'og:url', 'og:title', 'og:description', 'og:image')) {
|
||||
Get-Meta $key | Should -Not -BeNullOrEmpty -Because "$key is what the card is made of"
|
||||
}
|
||||
# Without it X renders the small square variant instead of a card.
|
||||
Get-Meta 'twitter:card' | Should -Be 'summary_large_image'
|
||||
}
|
||||
|
||||
It "gives absolute URLs, which the strict crawlers require" {
|
||||
foreach ($key in @('og:url', 'og:image', 'twitter:image')) {
|
||||
Get-Meta $key | Should -Match '^https://'
|
||||
}
|
||||
}
|
||||
|
||||
It "points og:url at the same place as the canonical" {
|
||||
$canonical = [regex]::Match($script:Html, '<link rel="canonical" href="([^"]*)">')
|
||||
$canonical.Success | Should -BeTrue
|
||||
Get-Meta 'og:url' | Should -Be $canonical.Groups[1].Value
|
||||
}
|
||||
|
||||
It "publishes the card beside the page" {
|
||||
# site/ is copied to the server wholesale; a card outside it is a 404
|
||||
# and the preview falls back to text.
|
||||
Test-Path -LiteralPath $script:Card | Should -BeTrue
|
||||
}
|
||||
|
||||
It "serves a card that is the size the tags claim" {
|
||||
# PNG header: width and height are big-endian at offsets 16 and 20.
|
||||
$bytes = [IO.File]::ReadAllBytes($script:Card)[0..23]
|
||||
$width = [int]$bytes[16] * 16777216 + [int]$bytes[17] * 65536 + [int]$bytes[18] * 256 + [int]$bytes[19]
|
||||
$height = [int]$bytes[20] * 16777216 + [int]$bytes[21] * 65536 + [int]$bytes[22] * 256 + [int]$bytes[23]
|
||||
$width | Should -Be 1200
|
||||
$height | Should -Be 630
|
||||
Get-Meta 'og:image:width' | Should -Be '1200'
|
||||
Get-Meta 'og:image:height' | Should -Be '630'
|
||||
}
|
||||
|
||||
It "does not advertise a command this repo does not ship" {
|
||||
$alt = Get-Meta 'og:image:alt'
|
||||
$alt | Should -Not -BeNullOrEmpty
|
||||
|
||||
$named = [regex]::Matches($alt, '\bz[a-z_]+\b') | ForEach-Object { $_.Value } | Sort-Object -Unique
|
||||
$named.Count | Should -BeGreaterThan 0 -Because 'the alt text names the commands on the card'
|
||||
|
||||
foreach ($cmd in $named) {
|
||||
if ($cmd -eq 'zscripts') { continue } # the toolkit, not a command
|
||||
$exists = @('.ps1', '.cmd') | Where-Object {
|
||||
Test-Path -LiteralPath (Join-Path $script:RepoRoot "$cmd$_")
|
||||
}
|
||||
$exists | Should -Not -BeNullOrEmpty -Because "$cmd is on the card but is not in this repo"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -38,14 +38,19 @@ Describe "plain-text twins" {
|
||||
Test-Path -LiteralPath $script:Generator | Should -BeTrue
|
||||
}
|
||||
|
||||
It "every .md that owes a twin has one" {
|
||||
$pairs = Select-String -Path $script:Generator -Pattern '^\s*\("([^"]+\.md)",\s*"([^"]+\.txt)"\),' |
|
||||
ForEach-Object { [pscustomobject]@{ Md = $_.Matches[0].Groups[1].Value; Txt = $_.Matches[0].Groups[2].Value } }
|
||||
# Asks the REPOSITORY what markdown it has, not the generator what it was
|
||||
# told about. Scraping the generator's own list could only ever prove the
|
||||
# list was self-consistent - a document nobody added to it was invisible to
|
||||
# the check, which is how ELEVATOR_PITCH.md and TOKEN_SAVINGS.md sat here
|
||||
# with no twin while this test passed.
|
||||
It "every .md at the repository root has a twin" {
|
||||
$mds = Get-ChildItem -LiteralPath $script:RepoRoot -Filter *.md -File
|
||||
|
||||
$pairs.Count | Should -BeGreaterThan 0 -Because "PAIRS in plaintext_twins.py is what this suite checks"
|
||||
foreach ($p in $pairs) {
|
||||
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Md) | Should -BeTrue -Because "$($p.Md) is listed in PAIRS"
|
||||
Test-Path -LiteralPath (Join-Path $script:RepoRoot $p.Txt) | Should -BeTrue -Because "$($p.Md) owes a twin at $($p.Txt)"
|
||||
$mds.Count | Should -BeGreaterThan 0 -Because "the repo documents itself in markdown"
|
||||
foreach ($md in $mds) {
|
||||
$txt = [IO.Path]::ChangeExtension($md.FullName, ".txt")
|
||||
Test-Path -LiteralPath $txt |
|
||||
Should -BeTrue -Because "$($md.Name) owes a twin at $(Split-Path -Leaf $txt)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
@ -89,6 +89,25 @@ Describe "public repo carries no private detail" {
|
||||
$hits | Should -BeNullOrEmpty -Because "these look like private detail copied in from the internal toolkit:`n$($hits -join "`n")"
|
||||
}
|
||||
|
||||
It "catches the current spelling <Sample>" -ForEach @(
|
||||
@{ Rule = 'current product name'; Sample = 'evo.ehs answers build_version' }
|
||||
@{ Rule = 'current product name'; Sample = 'evo-ai answers version on /health' }
|
||||
@{ Rule = 'current product name'; Sample = 'upstream evoehs_app:80' }
|
||||
@{ Rule = 'internal issue reference'; Sample = 'see evo.scripts#101 for the trap' }
|
||||
) {
|
||||
# The rename went past the old pattern: the dot and the hyphen break
|
||||
# the word and the underscore hides the boundary, so a suite that ran
|
||||
# green was trusted on a tree that named the fleet.
|
||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq $Rule }).Pattern
|
||||
$pattern | Should -Not -BeNullOrEmpty
|
||||
($Sample -match $pattern) | Should -BeTrue
|
||||
}
|
||||
|
||||
It "still allows this repo's own name" {
|
||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq 'current product name' }).Pattern
|
||||
('https://github.com/evomedia-net/evo.zscripts' -match $pattern) | Should -BeFalse
|
||||
}
|
||||
|
||||
It "still detects a planted violation" {
|
||||
# Mutation check. Without this the suite passes just as happily when the
|
||||
# patterns are broken as when the repo is clean - the failure mode that
|
||||
|
||||
240
tests/StartGitPull.Tests.ps1
Normal file
240
tests/StartGitPull.Tests.ps1
Normal file
@ -0,0 +1,240 @@
|
||||
# zstart's auto-pull must never stand between the user and a running server (#130).
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
# The report was "I merged it but not sure why it crashed, should have skipped
|
||||
# it and moved on". It crashed on a pull that SUCCEEDED:
|
||||
#
|
||||
# git : From https://github.com/example/some-app
|
||||
# At ZStart.ps1:206 char:24
|
||||
# + $pullOut = git pull --ff-only 2>&1
|
||||
#
|
||||
# Under $ErrorActionPreference = 'Stop', a stderr redirect on a native command
|
||||
# in Windows PowerShell 5.1 wraps every stderr line in a terminating
|
||||
# ErrorRecord - and git writes ordinary fetch progress ("From ...") to stderr.
|
||||
# So the try block died before its own "Auto-pull skipped" branch could run.
|
||||
#
|
||||
# These tests drive REAL git under 'Stop' on the same host the defect lives
|
||||
# on. A stand-in that faked git's output would prove nothing about the stream
|
||||
# semantics that are the entire bug; one test asserts the fixture really does
|
||||
# put that "From ..." line on stderr, so the reproduction cannot quietly go
|
||||
# stale the way an LF changelog fixture once did.
|
||||
#
|
||||
# ZHelpers.ps1 is dot-sourced rather than ZStart.ps1, which runs its main flow
|
||||
# on load. That is also why the logic moved into a helper: it was untestable
|
||||
# where it sat.
|
||||
|
||||
BeforeAll {
|
||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||
|
||||
$script:tmpRoots = New-Object System.Collections.ArrayList
|
||||
|
||||
function New-TempDir {
|
||||
param([string]$Tag)
|
||||
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zstart-pull-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
[void]$script:tmpRoots.Add($dir)
|
||||
return $dir
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
# Test plumbing only. Runs git quietly from a directory and fails the
|
||||
# test loudly if it did not work - the thing under test does its own
|
||||
# git handling and must not go through here.
|
||||
param([string]$In, [string[]]$GitArgs)
|
||||
Push-Location -LiteralPath $In
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# Quote anything with whitespace: a Windows temp root usually
|
||||
# sits under a user profile whose name has a space in it, and
|
||||
# an unquoted path splits into two arguments on the way through
|
||||
# cmd.
|
||||
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
|
||||
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
|
||||
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
|
||||
return $out
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
function New-ClonePair {
|
||||
# A bare "origin" and a working clone tracking main, one commit in.
|
||||
# Returns @{ Bare; Clone } and leaves a helper to advance origin.
|
||||
$bare = New-TempDir 'origin'
|
||||
Invoke-Git $bare @('init', '--bare', '--initial-branch=main', '--quiet') | Out-Null
|
||||
$seed = New-TempDir 'seed'
|
||||
Invoke-Git $seed @('init', '--initial-branch=main', '--quiet') | Out-Null
|
||||
Invoke-Git $seed @('config', 'user.email', 'test@example.invalid') | Out-Null
|
||||
Invoke-Git $seed @('config', 'user.name', 'zstart test') | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $seed 'a.txt') -Value 'one'
|
||||
Invoke-Git $seed @('add', '.') | Out-Null
|
||||
Invoke-Git $seed @('commit', '-q', '-m', 'one') | Out-Null
|
||||
Invoke-Git $seed @('remote', 'add', 'origin', $bare) | Out-Null
|
||||
Invoke-Git $seed @('push', '-q', '-u', 'origin', 'main') | Out-Null
|
||||
|
||||
$clone = New-TempDir 'clone'
|
||||
Invoke-Git (Split-Path -Parent $clone) @('clone', '-q', $bare, $clone) | Out-Null
|
||||
Invoke-Git $clone @('config', 'user.email', 'test@example.invalid') | Out-Null
|
||||
Invoke-Git $clone @('config', 'user.name', 'zstart test') | Out-Null
|
||||
return [pscustomobject]@{ Bare = $bare; Clone = $clone; Seed = $seed }
|
||||
}
|
||||
|
||||
function Add-OriginCommit {
|
||||
# Advance origin from the seed checkout, so the clone has something
|
||||
# to fetch - which is exactly what makes git print "From ..." on
|
||||
# stderr.
|
||||
param($Pair, [string]$Name = 'two')
|
||||
Set-Content -LiteralPath (Join-Path $Pair.Seed "$Name.txt") -Value $Name
|
||||
Invoke-Git $Pair.Seed @('add', '.') | Out-Null
|
||||
Invoke-Git $Pair.Seed @('commit', '-q', '-m', $Name) | Out-Null
|
||||
Invoke-Git $Pair.Seed @('push', '-q', 'origin', 'main') | Out-Null
|
||||
}
|
||||
|
||||
function Get-Head {
|
||||
param([string]$Repo)
|
||||
return (Invoke-Git $Repo @('rev-parse', 'HEAD') | Select-Object -Last 1).ToString().Trim()
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
foreach ($d in $script:tmpRoots) {
|
||||
try { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
Describe "Invoke-StartGitPull" {
|
||||
|
||||
Context "the reported case: origin has new commits" {
|
||||
|
||||
BeforeAll {
|
||||
$script:pair = New-ClonePair
|
||||
Add-OriginCommit $script:pair
|
||||
$script:originTip = Get-Head $script:pair.Seed
|
||||
}
|
||||
|
||||
It "the fixture really puts fetch progress on stderr - the shape of the bug" {
|
||||
# Checked on a second clone so the one under test is still behind.
|
||||
$probe = New-TempDir 'probe'
|
||||
Invoke-Git (Split-Path -Parent $probe) @('clone', '-q', $script:pair.Bare, $probe) | Out-Null
|
||||
Add-OriginCommit $script:pair 'three'
|
||||
Push-Location -LiteralPath $probe
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# stderr only: stdout is dropped, so anything captured came
|
||||
# from the stream that ErrorRecords are made from.
|
||||
$stderr = & cmd /c "git fetch origin 2>&1 1>nul"
|
||||
} finally { Pop-Location }
|
||||
($stderr -join "`n") | Should -Match '(?m)^From '
|
||||
}
|
||||
|
||||
It "does not abort under ErrorActionPreference = 'Stop'" {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:r = Invoke-StartGitPull -Root $script:pair.Clone } | Should -Not -Throw
|
||||
}
|
||||
|
||||
It "reports success" {
|
||||
$script:r.Ok | Should -BeTrue
|
||||
$script:r.Skipped | Should -BeFalse
|
||||
$script:r.Message | Should -Match '^Now at: '
|
||||
}
|
||||
|
||||
It "actually fast-forwarded the checkout" {
|
||||
Get-Head $script:pair.Clone | Should -Be (Get-Head $script:pair.Seed)
|
||||
}
|
||||
|
||||
It "leaves the caller's ErrorActionPreference as it found it" {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Invoke-StartGitPull -Root $script:pair.Clone | Out-Null
|
||||
$ErrorActionPreference | Should -Be 'Stop'
|
||||
}
|
||||
}
|
||||
|
||||
Context "nothing to fetch" {
|
||||
|
||||
It "is Ok and says so, not a skip" {
|
||||
$pair = New-ClonePair
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$r = Invoke-StartGitPull -Root $pair.Clone
|
||||
$r.Ok | Should -BeTrue
|
||||
$r.Message | Should -Be 'Already up to date.'
|
||||
}
|
||||
}
|
||||
|
||||
Context "a pull that cannot complete" {
|
||||
|
||||
It "diverged history is reported, not thrown, and the checkout is left alone" {
|
||||
$pair = New-ClonePair
|
||||
# Local commit on the clone AND a different one on origin.
|
||||
Set-Content -LiteralPath (Join-Path $pair.Clone 'local.txt') -Value 'mine'
|
||||
Invoke-Git $pair.Clone @('add', '.') | Out-Null
|
||||
Invoke-Git $pair.Clone @('commit', '-q', '-m', 'local') | Out-Null
|
||||
$localTip = Get-Head $pair.Clone
|
||||
Add-OriginCommit $pair 'theirs'
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:div = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
||||
$script:div.Ok | Should -BeFalse
|
||||
$script:div.Skipped | Should -BeFalse
|
||||
$script:div.Message | Should -Match 'cannot fast-forward'
|
||||
Get-Head $pair.Clone | Should -Be $localTip
|
||||
}
|
||||
|
||||
It "a branch with no upstream is skipped - and never switched away from" {
|
||||
$pair = New-ClonePair
|
||||
Invoke-Git $pair.Clone @('checkout', '-q', '-b', 'feature/thing') | Out-Null
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:noup = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
||||
$script:noup.Skipped | Should -BeTrue
|
||||
$script:noup.Message | Should -Match "no upstream"
|
||||
(Invoke-Git $pair.Clone @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -Last 1).ToString().Trim() |
|
||||
Should -Be 'feature/thing'
|
||||
}
|
||||
|
||||
It "a directory that is not a repo is skipped, not thrown" {
|
||||
$dir = New-TempDir 'norepo'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
{ $script:norepo = Invoke-StartGitPull -Root $dir } | Should -Not -Throw
|
||||
$script:norepo.Skipped | Should -BeTrue
|
||||
$script:norepo.Message | Should -Match 'not a git repo'
|
||||
}
|
||||
}
|
||||
|
||||
Context "housekeeping" {
|
||||
|
||||
It "restores GIT_TERMINAL_PROMPT to whatever it was" {
|
||||
$pair = New-ClonePair
|
||||
$prev = $env:GIT_TERMINAL_PROMPT
|
||||
try {
|
||||
$env:GIT_TERMINAL_PROMPT = 'sentinel'
|
||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
||||
$env:GIT_TERMINAL_PROMPT | Should -Be 'sentinel'
|
||||
} finally { $env:GIT_TERMINAL_PROMPT = $prev }
|
||||
}
|
||||
|
||||
It "returns to the directory it was called from" {
|
||||
$pair = New-ClonePair
|
||||
$here = (Get-Location).Path
|
||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
||||
(Get-Location).Path | Should -Be $here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Describe "ZStart.ps1 uses the helper" {
|
||||
|
||||
BeforeAll {
|
||||
$script:zstart = Get-Content -Raw (Join-Path (Split-Path -Parent $PSScriptRoot) "ZStart.ps1")
|
||||
}
|
||||
|
||||
It "no longer carries its own redirected pull - the line that crashed" {
|
||||
$script:zstart | Should -Not -Match 'git pull --ff-only 2>&1'
|
||||
}
|
||||
|
||||
It "calls Invoke-StartGitPull" {
|
||||
$script:zstart | Should -Match 'Invoke-StartGitPull -Root'
|
||||
}
|
||||
|
||||
It "still tells the user when it skipped, and how to stop it trying" {
|
||||
$script:zstart | Should -Match 'Auto-pull skipped'
|
||||
$script:zstart | Should -Match 'start\.gitPull=false'
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
# Deploy-verification planning (#101).
|
||||
# Deploy-verification planning.
|
||||
#
|
||||
# Invoke-Pester .\tests
|
||||
#
|
||||
@ -7,8 +7,8 @@
|
||||
# function (Get-VerifyAttempts) and are pinned here, where they can be tested
|
||||
# without an EC2 box: a project with no domain must never produce an edge
|
||||
# attempt, because an edge request with no Host header can only reach the
|
||||
# default vhost - which is a different product. That exact gap read evo.ehs's
|
||||
# build number during evo-ai deploys twice on 2026-08-31 alone.
|
||||
# default vhost - which is a different product. That exact gap read one
|
||||
# product's build number during another's deploys, twice in one day.
|
||||
#
|
||||
# ZHelpers.ps1 is dot-sourced rather than zdeploy.ps1: zdeploy executes its
|
||||
# main flow on load, helpers only define functions.
|
||||
@ -35,8 +35,8 @@ Describe "Get-VerifyAttempts" {
|
||||
($attempts | ForEach-Object Kind) | Should -Be @('exec', 'port', 'edge')
|
||||
}
|
||||
|
||||
It "never asks the edge for a project with no domain (the #101 trap)" {
|
||||
# The shape that hit #101: viaProxy + port, no domain. The old code
|
||||
It "never asks the edge for a project with no domain (the wrong-vhost trap)" {
|
||||
# The shape that hit it: viaProxy + port, no domain. The old code
|
||||
# fell back to the bare IP here and read another product's counter.
|
||||
$proj = New-Proj -Verify @{ viaProxy = "evo_edge_proxy"; upstream = "deploy-app-1:8000"; port = 8005; path = "/health" }
|
||||
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd "docker exec ..."
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
|
||||
|
||||
@ -23,9 +23,21 @@
|
||||
#>
|
||||
@{
|
||||
Denied = @(
|
||||
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|SmartPlant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' }
|
||||
@{ Name = 'private product domain'; Pattern = '\b(smartplantehs\.com|provensheet\.com|evoehs\.com|civilcode\.evomedia\.net|dashboard\.evomedia\.net|webmail\.evomedia\.net|mail-admin\.evomedia\.net|docketmail\.evomedia\.net|cardiff\.evomedia\.net|platform\.evomedia\.net|ai\.evomedia\.net|git\.evomedia\.net|analytics\.evomedia\.net)\b' }
|
||||
@{ Name = 'private-only script'; Pattern = '\b(register_civilcode|register_docketmail|sp_seed_demo_prod|zpublish_stats|zcoverage|zmerge|zpull|zresume|swag_set_owner|provision_demo|apply_platform_config_fixes)\b' }
|
||||
# The retired EHS name is split with a one-character class in both rules
|
||||
# below (Smart[P]lant, smart[p]lantehs). The regex is identical - a class of
|
||||
# one matches exactly that character - but the literal no longer appears in
|
||||
# this file, which is public. The private tree still carries that name in
|
||||
# ~20 places, so these rules are still load-bearing: do not delete them,
|
||||
# and do not un-split them.
|
||||
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|Smart[P]lant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' }
|
||||
# The current spellings, which the line above never saw: a dot or a
|
||||
# hyphen breaks the word and an underscore hides the boundary, so
|
||||
# evo.ehs, evo-ai and evoehs_app all passed (evo.scripts#138 in the
|
||||
# private tree). Internal issue references travel with them.
|
||||
@{ Name = 'current product name'; Pattern = '(?i)\bevo[.-](ehs|ai|edge|locate|proven|platform|civilcode|scripts)\b|\bevoehs' }
|
||||
@{ Name = 'internal issue reference'; Pattern = 'evo\.scripts#\d+' }
|
||||
@{ Name = 'private product domain'; Pattern = '\b(smart[p]lantehs\.com|provensheet\.com|evoehs\.com|civilcode\.evomedia\.net|dashboard\.evomedia\.net|webmail\.evomedia\.net|mail-admin\.evomedia\.net|docketmail\.evomedia\.net|cardiff\.evomedia\.net|platform\.evomedia\.net|ai\.evomedia\.net|git\.evomedia\.net|analytics\.evomedia\.net)\b' }
|
||||
@{ Name = 'private-only script'; Pattern = '\b(register_civilcode|register_docketmail|sp_seed_demo_prod|zpublish_stats|zcoverage|zresume|swag_set_owner|provision_demo|apply_platform_config_fixes)\b' }
|
||||
@{ Name = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' }
|
||||
@{ Name = 'operator home path'; Pattern = '/home/ubuntu/' }
|
||||
@{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' }
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# token-count.ps1 — measure script output volume to estimate AI agent token costs.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the
|
||||
# project's .env has a DATABASE_URL) into the backups folder.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup_ec2.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zbackup_ec2.ps1 — pull backups down from the server: a Postgres dump for projects
|
||||
# with a "db" config block, plus a zip of server-side data dirs (uploads/archive/dist).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
||||
#
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"deploy": {
|
||||
"zipName": "PyAppDeploy.zip",
|
||||
"gitPull": true,
|
||||
"tagOnDeploy": false,
|
||||
"exclude": [
|
||||
"docs"
|
||||
],
|
||||
@ -121,6 +122,7 @@
|
||||
"analytics": {
|
||||
"label": "Analytics (any docker compose app)",
|
||||
"kind": "docker",
|
||||
"_build_note": "Images pulled from a registry need nothing here. If the image is BUILT from a Dockerfile in this tree, add \"deploy\": { \"build\": true } \u2014 without it a redeploy uploads the new code and restarts the OLD image, and reports success.",
|
||||
"localRoot": "C:\\YourRoot\\analytics",
|
||||
"domain": "analytics.yourdomain.com",
|
||||
"remote": {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
||||
|
||||
321
zdeploy.ps1
321
zdeploy.ps1
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zdeploy.ps1 — deploy any project defined in zconfig.json to the server.
|
||||
# Each project runs its own docker compose stack; the handler is picked by the
|
||||
@ -9,6 +9,9 @@
|
||||
#
|
||||
# Usage:
|
||||
# zdeploy <project> [<project> ...] [-Note "message"]
|
||||
# zdeploy -Scan # report what is merged but not shipped, then offer to deploy it
|
||||
# zdeploy -s -Yes # same, unattended (no confirmation prompt)
|
||||
# zdeploy -s <project> ... # scan only these
|
||||
# zdeploy all # ztokens first, then every project (edge kinds next), stop at first failure
|
||||
# zdeploy ztokens # refresh the live-usage stats (see below)
|
||||
#
|
||||
@ -43,7 +46,13 @@
|
||||
param(
|
||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||
[string[]]$Projects = @(),
|
||||
[string]$Note = "Build deployed"
|
||||
[string]$Note = "Build deployed",
|
||||
# -Scan / -s: report which projects have work on the default branch that is
|
||||
# not live yet, then offer to deploy exactly those. See Get-DeployStatus.
|
||||
[Alias('s')][switch]$Scan,
|
||||
# Skip the confirmation prompt after a scan. Needed for unattended runs -
|
||||
# Read-Host has no answer in a non-interactive shell and would throw.
|
||||
[switch]$Yes
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@ -65,12 +74,169 @@ if (-not (Test-Path -LiteralPath $TempRoot)) {
|
||||
New-Item -ItemType Directory -Path $TempRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
# ── Scan: what is merged but not shipped ─────────────────────────────────────
|
||||
# Answers "which projects have work on the default branch that is not live?"
|
||||
#
|
||||
# WHAT IT COMPARES
|
||||
# ----------------
|
||||
# Every deploy leaves .last_deploy_sha and .last_deploy_utc in the project's
|
||||
# remote path. The SHA is the real answer: deployed commit versus the current
|
||||
# default-branch tip, exact regardless of clocks.
|
||||
#
|
||||
# .last_deploy_sha only exists from this change onward, so a project that has
|
||||
# not been deployed since falls back to comparing the tip's COMMIT TIME against
|
||||
# the deploy time. That is approximate on purpose and is labelled "~" in the
|
||||
# output: commit time is when the work was authored, not when it merged, so a
|
||||
# long-lived branch merged today carries an old timestamp and can read as
|
||||
# already-shipped. The fallback disappears the first time each project deploys.
|
||||
#
|
||||
# WHAT IT DOES NOT DO
|
||||
# -------------------
|
||||
# It does not judge whether the pending commits change anything shippable - a
|
||||
# README-only commit still reads as pending. Deploying that is wasteful, not
|
||||
# wrong, and the alternative (guessing which paths matter per project kind) is
|
||||
# the sort of cleverness that eventually skips a real change.
|
||||
function Get-DeployStatus {
|
||||
param([string[]]$Keys)
|
||||
|
||||
$cfgLocal = Get-ZConfig
|
||||
$rows = @()
|
||||
|
||||
# One ssh for every project rather than one each: this is a status read
|
||||
# people will run often, and 15 round trips to answer one question is the
|
||||
# difference between a habit and a chore. No $( ) and no embedded double
|
||||
# quotes - see the note on Invoke-Ec2Step.
|
||||
$parts = @()
|
||||
foreach ($k in $Keys) {
|
||||
$p = $cfgLocal.projects.$k
|
||||
if (-not ($p -and $p.remote -and $p.remote.path)) { continue }
|
||||
$rp = $p.remote.path
|
||||
$parts += "printf '$k\t'; cat $rp/.last_deploy_sha 2>/dev/null | tr -d '\n'; printf '\t'; cat $rp/.last_deploy_utc 2>/dev/null | tr -d '\n'; printf '\n';"
|
||||
}
|
||||
$remote = @{}
|
||||
if ($parts.Count -gt 0) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
# Deliberately not Invoke-Ec2Step: that prints a step header and throws
|
||||
# on failure. A scan wants the output captured, and a box that cannot be
|
||||
# reached should degrade to "unknown" rather than abort the report.
|
||||
$sshOpts = Get-Ec2SshOpts
|
||||
$lines = ssh @sshOpts -i $cfgLocal.ec2.pemKey (Get-Ec2Target) ($parts -join ' ') 2>&1 |
|
||||
ForEach-Object { "$_" }
|
||||
$ErrorActionPreference = $prev
|
||||
foreach ($line in $lines) {
|
||||
$f = $line -split "`t"
|
||||
if ($f.Count -ge 3) { $remote[$f[0]] = @{ Sha = $f[1].Trim(); Utc = $f[2].Trim() } }
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($k in $Keys) {
|
||||
$p = $cfgLocal.projects.$k
|
||||
$row = [ordered]@{ Key = $k; Kind = $p.kind; State = ''; Detail = ''; Ahead = 0 }
|
||||
$root = $p.localRoot
|
||||
|
||||
# Not a test for .git in $root: a localRoot may point INTO a repo
|
||||
# rather than at its top, when the deployable app is a subdirectory of
|
||||
# the checkout. Testing for the folder reported every such project as
|
||||
# having no checkout at all. Let git walk up instead.
|
||||
$isRepo = $false
|
||||
if ($root -and (Test-Path -LiteralPath $root)) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git -C $root rev-parse --is-inside-work-tree 2>$null | Out-Null
|
||||
$isRepo = ($LASTEXITCODE -eq 0)
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
if (-not $isRepo) {
|
||||
$row.State = 'no-repo'; $row.Detail = 'no git checkout'
|
||||
$rows += [pscustomobject]$row; continue
|
||||
}
|
||||
|
||||
Push-Location -LiteralPath $root
|
||||
try {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
git fetch origin --prune --quiet
|
||||
$default = (git symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
|
||||
if (-not $default) { $default = 'main' }
|
||||
$tip = (git rev-parse "origin/$default" 2>$null)
|
||||
$tipUtc = (git show -s --format=%cI "origin/$default" 2>$null)
|
||||
# Same exclusions as the deploy's own guard, or the scan would
|
||||
# report a blocker zdeploy would happily run through: untracked
|
||||
# files ship anyway, and build-version.json / CHANGELOG.md are
|
||||
# written BY a deploy.
|
||||
$dirty = git status --porcelain --untracked-files=no | Where-Object {
|
||||
$name = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||
@('build-version.json', 'CHANGELOG.md') -notcontains $name
|
||||
}
|
||||
$ErrorActionPreference = $prev
|
||||
|
||||
if (-not $tip) { $row.State = 'no-repo'; $row.Detail = "no origin/$default"; $rows += [pscustomobject]$row; continue }
|
||||
|
||||
$r = $remote[$k]
|
||||
if (-not $r) {
|
||||
$row.State = 'unknown'; $row.Detail = 'box unreachable'
|
||||
}
|
||||
elseif ($r.Sha) {
|
||||
if ($r.Sha -eq $tip) { $row.State = 'current'; $row.Detail = $tip.Substring(0, 7) }
|
||||
else {
|
||||
$row.State = 'PENDING'
|
||||
$n = (git rev-list --count "$($r.Sha)..origin/$default" 2>$null)
|
||||
if (-not $n -or $LASTEXITCODE -ne 0) { $n = '?' } # deployed SHA not in this repo's history
|
||||
$row.Ahead = $n
|
||||
$row.Detail = "$n commit(s) since $($r.Sha.Substring(0, [Math]::Min(7, $r.Sha.Length)))"
|
||||
}
|
||||
}
|
||||
elseif (-not $r.Utc) {
|
||||
# NOT pending. No stamp means this project has never been
|
||||
# deployed by a zdeploy that wrote one - which says nothing
|
||||
# about whether it is behind. Calling it pending would have
|
||||
# swept edge, the mail server and monitoring into an unattended
|
||||
# run on no evidence at all, and edge in particular does not
|
||||
# take a speculative deploy well. Deploy it once by name to set
|
||||
# the baseline; every scan after that is exact.
|
||||
$row.State = 'no-stamp'; $row.Detail = 'no deploy stamp - deploy once by name to baseline it'
|
||||
}
|
||||
else {
|
||||
# Timestamp fallback - approximate, flagged with ~.
|
||||
$deployedAt = [datetime]::MinValue
|
||||
$ok = [datetime]::TryParse(($r.Utc -replace ' UTC$', ''), [ref]$deployedAt)
|
||||
$tipAt = [datetime]::MinValue
|
||||
$ok2 = [datetime]::TryParse($tipUtc, [ref]$tipAt)
|
||||
if ($ok -and $ok2 -and $tipAt.ToUniversalTime() -gt $deployedAt) {
|
||||
$row.State = 'PENDING'
|
||||
$row.Ahead = '~'
|
||||
$row.Detail = "~ tip $(($tipAt.ToUniversalTime()).ToString('MM-dd HH:mm')) > deploy $($r.Utc -replace ' UTC$','')"
|
||||
}
|
||||
else {
|
||||
$row.State = 'current'; $row.Detail = "~ deployed $($r.Utc -replace ' UTC$','')"
|
||||
}
|
||||
}
|
||||
|
||||
if ($dirty -and $row.State -eq 'PENDING') {
|
||||
$row.State = 'BLOCKED'
|
||||
$row.Detail = "$(@($dirty).Count) uncommitted file(s) - deploy would refuse"
|
||||
}
|
||||
}
|
||||
finally { Pop-Location }
|
||||
|
||||
$rows += [pscustomobject]$row
|
||||
}
|
||||
return $rows
|
||||
}
|
||||
|
||||
# A bare `zdeploy -s` means "look at everything", so it must not fall into the
|
||||
# usage block below.
|
||||
if ($Scan -and $Projects.Count -eq 0) { $Projects = @(Get-ZProjectKeys) }
|
||||
|
||||
if ($Projects.Count -eq 0) {
|
||||
$keys = (Get-ZProjectKeys) -join ', '
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zdeploy <project> [<project> ...] | all | ztokens [-Note `"message`"]" -ForegroundColor Yellow
|
||||
Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray
|
||||
Write-Host " 'all' deploys everything (edge kinds first) and stops at the first failure." -ForegroundColor Gray
|
||||
Write-Host " -Scan / -s reports which projects have merged work that is not live, then offers to deploy just those." -ForegroundColor Gray
|
||||
Write-Host " Add -Yes to skip the confirmation prompt. Edge still ships first." -ForegroundColor Gray
|
||||
Write-Host " 'ztokens' refreshes the live-usage stats published to the zscripts page." -ForegroundColor Gray
|
||||
Stop-ZTracking; exit 1
|
||||
}
|
||||
@ -90,6 +256,80 @@ if ($Projects -contains 'all') {
|
||||
# do the risky order, because this sort only ran for 'all'.
|
||||
# Order within each group is preserved, so an intentional sequence still holds
|
||||
# — notably `zdeploy ztokens evo`, where ztokens must still precede evo.
|
||||
# Scan runs BEFORE the edge-first sort below, so whatever it selects still gets
|
||||
# ordered by that rule - the proxy ships before the apps behind it, exactly as
|
||||
# a hand-typed list would.
|
||||
if ($Scan) {
|
||||
Write-Host "`n=== zdeploy -Scan: what is merged but not shipped ===" -ForegroundColor Cyan
|
||||
$status = Get-DeployStatus -Keys @($Projects | Where-Object { $_ -ne 'ztokens' })
|
||||
|
||||
Write-Host ""
|
||||
foreach ($r in $status) {
|
||||
$colour = switch ($r.State) {
|
||||
'PENDING' { 'Yellow' }
|
||||
'BLOCKED' { 'Red' }
|
||||
'no-stamp' { 'DarkYellow' }
|
||||
'current' { 'DarkGray' }
|
||||
default { 'DarkYellow' }
|
||||
}
|
||||
Write-Host (" {0,-14} {1,-8} {2,-9} {3}" -f $r.Key, $r.Kind, $r.State, $r.Detail) -ForegroundColor $colour
|
||||
}
|
||||
|
||||
$pending = @($status | Where-Object { $_.State -eq 'PENDING' })
|
||||
$blocked = @($status | Where-Object { $_.State -eq 'BLOCKED' })
|
||||
$unknown = @($status | Where-Object { $_.State -eq 'unknown' })
|
||||
$nostamp = @($status | Where-Object { $_.State -eq 'no-stamp' })
|
||||
Write-Host ""
|
||||
Write-Host (" {0} pending, {1} blocked, {2} no-stamp, {3} unknown, {4} current" -f `
|
||||
$pending.Count, $blocked.Count, $nostamp.Count, $unknown.Count,
|
||||
@($status | Where-Object { $_.State -eq 'current' }).Count) -ForegroundColor Gray
|
||||
|
||||
if ($blocked.Count -gt 0) {
|
||||
Write-Host " Blocked projects are NOT deployed - commit or stash them, then re-run." -ForegroundColor Red
|
||||
}
|
||||
if ($nostamp.Count -gt 0) {
|
||||
Write-Host " No-stamp projects are NOT selected - deploy each once by name to establish a baseline." -ForegroundColor DarkYellow
|
||||
}
|
||||
if ($unknown.Count -gt 0) {
|
||||
# Silence here would read as "nothing to do", which is the one thing an
|
||||
# unreachable box does not mean.
|
||||
Write-Host " Unknown = the box did not answer for that project; its state is NOT 'current'." -ForegroundColor DarkYellow
|
||||
}
|
||||
|
||||
if ($pending.Count -eq 0) {
|
||||
Write-Host "`n Nothing to deploy.`n" -ForegroundColor Green
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if (-not $Yes) {
|
||||
# Two different ways a prompt can have nobody to answer it, and they
|
||||
# fail differently:
|
||||
# - a -NonInteractive host: Read-Host THROWS. Caught below.
|
||||
# - redirected stdin (a pipe, a scheduled task, powershell.exe launched
|
||||
# from another shell): Read-Host does NOT throw - it BLOCKS, waiting
|
||||
# on a pipe that never answers. The first scan run this way sat for
|
||||
# ten minutes with its table already printed but withheld behind the
|
||||
# blocked pipeline. [Environment]::UserInteractive is $true in both
|
||||
# cases, so it cannot be the test; IsInputRedirected can.
|
||||
if ([Console]::IsInputRedirected) {
|
||||
Write-Host " stdin is not a terminal - cannot prompt. Re-run with -Yes to deploy these $($pending.Count).`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
$answer = $null
|
||||
try { $answer = Read-Host " Deploy these $($pending.Count)? [y/N]" }
|
||||
catch {
|
||||
Write-Host " Non-interactive shell - cannot prompt. Re-run with -Yes to deploy these $($pending.Count).`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
if ($answer -notmatch '^(y|yes)$') {
|
||||
Write-Host " Aborted. Nothing deployed.`n" -ForegroundColor Yellow
|
||||
Stop-ZTracking; exit 0
|
||||
}
|
||||
}
|
||||
$Projects = @($pending | ForEach-Object { $_.Key })
|
||||
}
|
||||
|
||||
$requested = @($Projects)
|
||||
$edgeKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -eq 'edge' })
|
||||
$restKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -ne 'edge' })
|
||||
@ -101,6 +341,34 @@ if ($Projects.Count -gt 1) {
|
||||
Write-Host "Deploying: $($Projects -join ', ')$note" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# The bash that stamps what just shipped. The timestamp has always been
|
||||
# written; the SHA is what lets -Scan answer exactly rather than by clock
|
||||
# comparison. Omitted rather than faked when the checkout is not a git repo -
|
||||
# scan falls back to the timestamp, and a wrong SHA would be worse than none.
|
||||
function Get-RecordDeployBash {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Proj,
|
||||
[Parameter(Mandatory)][string]$RemotePath,
|
||||
# Optional: the edge, static and docker paths upload no zip, so there
|
||||
# is nothing to remove - but they still ship, so they still stamp.
|
||||
[string]$ZipName = ''
|
||||
)
|
||||
$sha = ''
|
||||
$root = $Proj.localRoot
|
||||
if ($root -and (Test-Path -LiteralPath (Join-Path $root '.git'))) {
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$sha = (git -C $root rev-parse HEAD 2>$null)
|
||||
if ($LASTEXITCODE -ne 0) { $sha = '' }
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
$cmd = "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $RemotePath/.last_deploy_utc > /dev/null"
|
||||
# 40 hex characters, so it needs no quoting in the remote command.
|
||||
if ($sha) { $cmd += " && printf '%s' $sha | sudo tee $RemotePath/.last_deploy_sha > /dev/null" }
|
||||
if ($ZipName) { $cmd += " && rm -f $RemoteHome/$ZipName" }
|
||||
return $cmd
|
||||
}
|
||||
|
||||
function Get-DeployZipName {
|
||||
param([string]$Key, $Proj)
|
||||
if ($Proj.deploy -and $Proj.deploy.zipName) { return $Proj.deploy.zipName }
|
||||
@ -278,6 +546,16 @@ function Wait-VerifyStaticBuild {
|
||||
# advances deliberately — one version bump per release — so "is the
|
||||
# build I just packed live?" means an exact match.
|
||||
$expectedLabel = Get-LabelFromBuildJsonObj $PreZipBuildState
|
||||
# A stamp we cannot read is not a version to check against. Comparing an
|
||||
# unreadable local label to an unreadable remote one is how a verification
|
||||
# once passed while the container served anything it liked, so refuse to
|
||||
# run rather than run a comparison that cannot fail.
|
||||
if ([string]::IsNullOrWhiteSpace($expectedLabel)) {
|
||||
Write-Host "`n--- [$Key version] NOT VERIFIED - build-version.json is present but unreadable ---" -ForegroundColor Yellow
|
||||
Write-Host " Got: $(($PreZipBuildState | ConvertTo-Json -Compress -Depth 4))" -ForegroundColor DarkGray
|
||||
Write-Host " Expected one of: {`"version`":`"v1.0.0.0.0`"} | {major,rc,beta,alpha,build} | {productVersion,buildNumber}" -ForegroundColor DarkGray
|
||||
return
|
||||
}
|
||||
Write-Host "`n--- [$Key] Live build verification (expect $expectedLabel) ---" -ForegroundColor Cyan
|
||||
$containerName = $Proj.remote.containerName
|
||||
$deadline = (Get-Date).AddSeconds(45)
|
||||
@ -297,7 +575,7 @@ function Wait-VerifyStaticBuild {
|
||||
}
|
||||
if ($r) {
|
||||
$remoteLabel = Get-LabelFromBuildJsonObj $r
|
||||
if ($remoteLabel -eq $expectedLabel) {
|
||||
if ($remoteLabel -and $remoteLabel -eq $expectedLabel) {
|
||||
Write-Host " PASS - live build $remoteLabel matches expected." -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
@ -507,7 +785,7 @@ function Invoke-PythonDeploy {
|
||||
Invoke-Ec2Step "require compose directory" "test -d $composeDir"
|
||||
Invoke-Ec2Step "docker compose build $appSvc" "cd $composeDir && sudo COMPOSE_BAKE=false docker compose build $appSvc"
|
||||
Invoke-Ec2Step "docker compose up -d" "cd $composeDir && sudo COMPOSE_BAKE=false docker compose up -d"
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
|
||||
if ($hasVersionTool) {
|
||||
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
||||
@ -557,6 +835,10 @@ function Invoke-PythonDeploy {
|
||||
-FailHint "App restart after the build bump failed."
|
||||
|
||||
Wait-VerifyApiBuild -Key $Key -Proj $Proj -ExpectedLabel $BuildVersion -TimeoutSec 30 | Out-Null
|
||||
|
||||
# Only now: the tag is a claim about what is RUNNING, so it
|
||||
# is written after the live build has been proven, never before.
|
||||
New-DeployTag -Proj $Proj -Version $BuildVersion -Note $ChangeNote
|
||||
} elseif ($Proj.verify -and $Proj.verify.port) {
|
||||
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
||||
} elseif ($Proj.domain) {
|
||||
@ -647,7 +929,7 @@ function Invoke-ViteDeploy {
|
||||
if ($edgeProj -and $edgeProj.Config.proxyContainer) {
|
||||
Invoke-Ec2Step "reload edge nginx (flush DNS cache for new container IP)" "sudo docker exec $($edgeProj.Config.proxyContainer) nginx -s reload"
|
||||
}
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
Invoke-Ec2Step "record deploy time; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
|
||||
Wait-VerifyStaticBuild -Key $Key -Proj $Proj -PreZipBuildState $preZipBuild
|
||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||
@ -773,7 +1055,7 @@ function Invoke-NextDeploy {
|
||||
if ($Proj.migrations -eq "prisma") {
|
||||
Invoke-Ec2Step "apply prisma migrations" "cd $composeDir && sudo docker compose exec -T $appSvc npx prisma migrate deploy"
|
||||
}
|
||||
Invoke-Ec2Step "record deploy timestamp; remove remote zip" "date -u +'%Y-%m-%d %H:%M:%S UTC' | sudo tee $remotePath/.last_deploy_utc > /dev/null && rm -f $RemoteHome/$zipName"
|
||||
Invoke-Ec2Step "record deploy timestamp; remove remote zip" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
||||
|
||||
Write-Host "`n--- [5] Verifying deployment ---" -ForegroundColor Cyan
|
||||
# Same precedence as the python handler. Do NOT probe http://<ec2-ip>:<prod-port>/
|
||||
@ -886,6 +1168,7 @@ function Invoke-EdgeDeploy {
|
||||
Invoke-Ec2Step "edge nginx reload" "sudo docker exec $pc nginx -s reload || true"
|
||||
}
|
||||
Invoke-Ec2Step "fix nginx-logs permissions (if present)" "if [ -d $remotePath/nginx-logs ]; then sudo chmod 777 $remotePath/nginx-logs; sudo chmod 666 $remotePath/nginx-logs/*.log 2>/dev/null || true; fi"
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "--- [Done] Edge proxy deploy finished ---" -ForegroundColor Green
|
||||
}
|
||||
|
||||
@ -893,8 +1176,8 @@ function Invoke-StaticDeploy {
|
||||
param([string]$Key, $Proj)
|
||||
|
||||
# Plain static sites - no build, no container of their own. The landing
|
||||
# container serves them straight off disk out of /srv/$host, so shipping
|
||||
# the files IS the deploy: there is nothing to restart afterwards.
|
||||
# container serves them straight off disk, one directory per host, so
|
||||
# shipping the files IS the deploy: there is nothing to restart afterwards.
|
||||
$root = Join-Path $Proj.localRoot $Proj.siteDir
|
||||
$remotePath = $Proj.remote.path
|
||||
|
||||
@ -923,6 +1206,7 @@ function Invoke-StaticDeploy {
|
||||
Invoke-Ec2Step "swap in $($d.Name)" "rm -rf $remotePath/$($d.Name) && mv $remotePath/.staging-$($d.Name) $remotePath/$($d.Name)"
|
||||
}
|
||||
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "--- [Done] Static site deploy finished ---" -ForegroundColor Green
|
||||
}
|
||||
|
||||
@ -970,10 +1254,14 @@ function Invoke-DockerDeploy {
|
||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
Invoke-Ec2Step "docker compose pull" "cd $remotePath && sudo docker compose pull"
|
||||
# Built here or pulled from a registry - see Get-DockerImageStep for why
|
||||
# a build-from-source stack cannot use `pull` and silently ships nothing.
|
||||
$imageStep = Get-DockerImageStep -Proj $Proj -RemotePath $remotePath
|
||||
Invoke-Ec2Step $imageStep.Label $imageStep.Command
|
||||
Invoke-Ec2Step "docker compose up -d" "cd $remotePath && sudo docker compose up -d"
|
||||
|
||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||
Invoke-Ec2Step "record deploy stamp" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath)
|
||||
Write-Host "`n--- [Done] $($Proj.label) deploy finished ---" -ForegroundColor Green
|
||||
Write-DeployLocation -Proj $Proj
|
||||
}
|
||||
@ -1016,6 +1304,16 @@ function Invoke-ZTokensPublish {
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
||||
|
||||
# pip, uv and docker draw progress bars with box-drawing characters: "━" is
|
||||
# the bytes E2 94 81. PowerShell 5.1 decodes a native command's stdout - ssh's,
|
||||
# here - with [Console]::OutputEncoding, which on Windows is the OEM code page
|
||||
# (437 on this machine), where those three bytes read "Γöü". Forty per bar.
|
||||
# Decode the box's output as the UTF-8 it is for the duration of the deploy,
|
||||
# and put the console back in the finally so a deploy that throws does not
|
||||
# leave the session changed.
|
||||
$prevConsoleEncoding = [Console]::OutputEncoding
|
||||
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
||||
try {
|
||||
foreach ($key in $Projects) {
|
||||
# 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old
|
||||
# singular 'ztoken' still works so existing habits and any script that
|
||||
@ -1033,6 +1331,9 @@ foreach ($key in $Projects) {
|
||||
default { throw "No deploy handler for kind '$($proj.kind)' (project '$key'). Add an Invoke-<Kind>Deploy function in zdeploy.ps1." }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
[Console]::OutputEncoding = $prevConsoleEncoding
|
||||
}
|
||||
# The timestamp goes through Stop-ZTracking as the FinalNote so it lands after
|
||||
# the tracking footer and before the trailing blank lines - the last thing on
|
||||
# screen, which is the point: scroll to the bottom and you can see how long ago
|
||||
|
||||
2
zec2.cmd
2
zec2.cmd
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*
|
||||
|
||||
18
zec2.ps1
18
zec2.ps1
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||
#
|
||||
@ -22,9 +22,11 @@ Start-ZTracking
|
||||
|
||||
$cfg = Get-ZConfig
|
||||
if (-not $HostName) { $HostName = $cfg.ec2.ip }
|
||||
# Needed by the container-side version read below; same names zec2online.ps1
|
||||
# uses. Without them that read throws inside its try/catch and falls through
|
||||
# silently, which looks identical to a service that cannot be reached.
|
||||
# Needed by the container-side version read below. zec2 had no ssh of its own
|
||||
# before that, so the read referenced three variables this script never
|
||||
# defined -- and because it sits inside a try/catch, the failure was silent:
|
||||
# it fell through to the HTTP call and reported nothing once that endpoint
|
||||
# stopped being public. Same names zec2online.ps1 uses.
|
||||
$PemKey = $cfg.ec2.pemKey
|
||||
$SshTarget = Get-Ec2Target
|
||||
|
||||
@ -103,9 +105,9 @@ function Show-Zec2LiveVersion {
|
||||
$r = Invoke-RestMethod -Uri "http://${HostName}/build-version.json" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
||||
if ($r) { Write-Host " Live build: $(Get-LabelFromBuildJsonObj $r)" -ForegroundColor Gray }
|
||||
} else {
|
||||
# Container-side first where the project configures it: a build
|
||||
# stamp is not public on every site, and asking the proxy answers
|
||||
# from whichever vhost matches the Host header.
|
||||
# Container-side first where the project configures it: the
|
||||
# endpoint is not public on every project, and asking the edge
|
||||
# answers from whichever vhost matches the Host header.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
$label = $null
|
||||
if ($execCmd -and (Test-Path $PemKey)) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zec2_rotatekeys.ps1 - rotate / reset secret keys in a project's SERVER-SIDE
|
||||
# .env, in place, without the values ever passing through this machine's shell
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2online.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zec2online.ps1 — deep health check: verify apps are live AND running the expected
|
||||
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||
@ -86,9 +86,10 @@ function Get-RemoteVersionLabel {
|
||||
param($Proj)
|
||||
$headers = @{}
|
||||
if ($Proj.domain) { $headers['Host'] = $Proj.domain }
|
||||
# Container-side first where the project configures it. A build stamp is
|
||||
# not public on every site, and the proxy answers from whichever vhost
|
||||
# matches the Host header - which is how a check reads another service.
|
||||
# Container-side first where the project configures it. The endpoint is
|
||||
# not public on every project, and the edge answers from whichever vhost
|
||||
# matches the Host header -- which is how a check reads another
|
||||
# product's version.
|
||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||
if ($execCmd -and (Test-Path $PemKey)) {
|
||||
try {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKillOnly.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through.
|
||||
& (Join-Path $PSScriptRoot "ZKillOnly.ps1") @args
|
||||
|
||||
251
zmerge.ps1
Normal file
251
zmerge.ps1
Normal file
@ -0,0 +1,251 @@
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zmerge.ps1 - merge the fleet's ready pull requests in one pass.
|
||||
#
|
||||
# Usage:
|
||||
# zmerge dry run: list every open PR and its verdict
|
||||
# zmerge -Execute merge everything that is genuinely ready
|
||||
# zmerge -e the same; -e is an alias, as -s is for -Scan
|
||||
# zmerge -Exclude 431 skip PRs by number (repeatable)
|
||||
# zmerge -Repo <name> limit to one repo
|
||||
# zmerge -Only 68,67 merge just these
|
||||
# zmerge -Execute -Yes skip the confirmation prompt
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# ---------------
|
||||
# Eleven ready PRs across three repos is eleven trips through the GitHub UI, and
|
||||
# the failure mode is not the clicking - it is that `gh pr create` and the merge
|
||||
# button will both happily accept a PR that cannot actually merge. Mergeability
|
||||
# is computed asynchronously, so a PR reports UNKNOWN for a few seconds after any
|
||||
# push and CONFLICTING only later. Merging by hand, the tenth PR is the one that
|
||||
# gets rubber-stamped.
|
||||
#
|
||||
# So this refuses to merge anything it has not just re-checked, and it re-checks
|
||||
# after every merge, because merging one PR can conflict another in the same
|
||||
# repo.
|
||||
#
|
||||
# WHAT IT WILL NOT DO
|
||||
# -------------------
|
||||
# * merge a PR that is not MERGEABLE/CLEAN at the moment it is reached
|
||||
# * merge a draft, or one with a failing required check
|
||||
# * bump versions - each repo stamps differently (zbump for zscripts,
|
||||
# bump_build_version.mjs for www), and a wrong stamp is worse than none.
|
||||
# The follow-up commands are printed instead.
|
||||
# * deploy anything. Deploys are run by hand, deliberately.
|
||||
#
|
||||
# ON UNKNOWN
|
||||
# ----------
|
||||
# GitHub returns mergeable=UNKNOWN while it computes, which is indistinguishable
|
||||
# from trouble if you only look once. Each PR is polled up to $PollTries times
|
||||
# before being treated as not ready, so a slow answer does not read as a failure
|
||||
# and a real CONFLICTING never reads as "probably fine".
|
||||
|
||||
param(
|
||||
[Alias('e')][switch]$Execute,
|
||||
[switch]$Yes,
|
||||
[int[]]$Exclude = @(),
|
||||
[int[]]$Only = @(),
|
||||
[string]$Repo,
|
||||
[int]$PollTries = 6,
|
||||
[int]$PollDelaySeconds = 4
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Two blank lines at the end of a run, matching every other z-script, so output
|
||||
# is separated from the next prompt. Local copy rather than ZHelpers: this
|
||||
# script does not dot-source it.
|
||||
function Write-ZTrailer { Write-Host ""; Write-Host "" }
|
||||
|
||||
# Every repository in the org, asked of GitHub rather than remembered here.
|
||||
#
|
||||
# Local to this script for the same reason Write-ZTrailer is: zmerge needs gh
|
||||
# and nothing else, and dot-sourcing 1,200 lines of deploy helpers for one
|
||||
# function would trade that away.
|
||||
#
|
||||
# THROWS rather than returning an empty list when gh fails. A merge tool that
|
||||
# quietly scans nothing prints exactly the same reassuring line as one that
|
||||
# scanned everything and found nothing, and those two must never be
|
||||
# confusable - which is precisely how the list this replaced hid its own rot.
|
||||
function Get-FleetRepos {
|
||||
param([Parameter(Mandatory)][string]$Org)
|
||||
$raw = & gh repo list $Org --limit 200 --json name,isArchived 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "gh repo list $Org failed ($LASTEXITCODE): $($raw -join ' ')"
|
||||
}
|
||||
try { $all = $raw | ConvertFrom-Json } catch {
|
||||
throw "gh repo list $Org did not return JSON: $($raw -join ' ')"
|
||||
}
|
||||
if (-not $all) { throw "gh repo list $Org returned no repositories" }
|
||||
$names = @($all | Where-Object { -not $_.isArchived } |
|
||||
ForEach-Object { $_.name } | Sort-Object)
|
||||
if ($names.Count -eq 0) { throw "every repository in $Org is archived?" }
|
||||
return $names
|
||||
}
|
||||
|
||||
|
||||
|
||||
$ORG = "evomedia-net"
|
||||
# Discovered, never listed. The list this replaced had fallen fourteen
|
||||
# repositories behind: a scan covered sixteen of thirty and said "Nothing open
|
||||
# to merge" while a ready PR sat in one of the fourteen it could not see.
|
||||
$REPOS = Get-FleetRepos -Org $ORG
|
||||
|
||||
# How each repo advances its build stamp after a merge. Printed as follow-up,
|
||||
# never run: see the header.
|
||||
$BUMP = @{
|
||||
"evo.zscripts" = "zbump"
|
||||
"evo.www" = "node scripts/bump_build_version.mjs bump (on main, then push)"
|
||||
}
|
||||
|
||||
function Invoke-Gh {
|
||||
param([string[]]$GhArgs, [switch]$AllowFail)
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$out = & gh @GhArgs 2>&1 | ForEach-Object { "$_" }
|
||||
$code = $LASTEXITCODE
|
||||
} finally { $ErrorActionPreference = $prev }
|
||||
if ($code -ne 0 -and -not $AllowFail) {
|
||||
throw "gh $($GhArgs -join ' ') failed ($code): $($out -join "`n")"
|
||||
}
|
||||
return [pscustomobject]@{ Output = ($out -join "`n"); Code = $code }
|
||||
}
|
||||
|
||||
function Get-OpenPrs {
|
||||
param([string]$RepoName)
|
||||
$r = Invoke-Gh @("pr", "list", "-R", "$ORG/$RepoName", "--state", "open",
|
||||
"--limit", "100", "--json", "number,title,isDraft,headRefName") -AllowFail
|
||||
if ($r.Code -ne 0 -or -not $r.Output) { return @() }
|
||||
return @($r.Output | ConvertFrom-Json)
|
||||
}
|
||||
|
||||
# Re-checked immediately before every merge, and again after each one, because
|
||||
# merging into the default branch can conflict a sibling PR in the same repo.
|
||||
function Get-Readiness {
|
||||
param([string]$RepoName, [int]$Number)
|
||||
for ($i = 1; $i -le $PollTries; $i++) {
|
||||
$r = Invoke-Gh @("pr", "view", "$Number", "-R", "$ORG/$RepoName",
|
||||
"--json", "mergeable,mergeStateStatus,state,isDraft") -AllowFail
|
||||
if ($r.Code -ne 0) { return [pscustomobject]@{ Ready = $false; Why = "cannot read PR" } }
|
||||
$j = $r.Output | ConvertFrom-Json
|
||||
if ($j.state -ne "OPEN") { return [pscustomobject]@{ Ready = $false; Why = "state is $($j.state)" } }
|
||||
if ($j.isDraft) { return [pscustomobject]@{ Ready = $false; Why = "draft" } }
|
||||
if ($j.mergeable -eq "MERGEABLE" -and $j.mergeStateStatus -eq "CLEAN") {
|
||||
return [pscustomobject]@{ Ready = $true; Why = "MERGEABLE/CLEAN" }
|
||||
}
|
||||
if ($j.mergeable -eq "CONFLICTING") {
|
||||
return [pscustomobject]@{ Ready = $false; Why = "CONFLICTING - rebase it" }
|
||||
}
|
||||
# UNKNOWN, or a non-CLEAN state such as BLOCKED/BEHIND: give GitHub a
|
||||
# moment, since it computes mergeability asynchronously.
|
||||
if ($j.mergeable -ne "UNKNOWN" -and $j.mergeStateStatus -ne "UNKNOWN") {
|
||||
return [pscustomobject]@{ Ready = $false; Why = "$($j.mergeable)/$($j.mergeStateStatus)" }
|
||||
}
|
||||
Start-Sleep -Seconds $PollDelaySeconds
|
||||
}
|
||||
return [pscustomobject]@{ Ready = $false; Why = "still UNKNOWN after $PollTries tries" }
|
||||
}
|
||||
|
||||
$targets = if ($Repo) { @($Repo) } else { $REPOS }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Scanning $($targets.Count) repo(s) for open pull requests..." -ForegroundColor Cyan
|
||||
|
||||
$queue = @()
|
||||
foreach ($r in $targets) {
|
||||
foreach ($pr in (Get-OpenPrs -RepoName $r)) {
|
||||
if ($Exclude -contains $pr.number) { continue }
|
||||
if ($Only.Count -gt 0 -and $Only -notcontains $pr.number) { continue }
|
||||
$queue += [pscustomobject]@{ Repo = $r; Number = $pr.number; Title = $pr.title; Draft = $pr.isDraft }
|
||||
}
|
||||
}
|
||||
|
||||
if ($queue.Count -eq 0) { Write-Host "Nothing open to merge." -ForegroundColor Yellow; Write-ZTrailer; exit 0 }
|
||||
|
||||
Write-Host ""
|
||||
foreach ($p in $queue) {
|
||||
$v = Get-Readiness -RepoName $p.Repo -Number $p.Number
|
||||
$p | Add-Member -NotePropertyName Ready -NotePropertyValue $v.Ready -Force
|
||||
$p | Add-Member -NotePropertyName Why -NotePropertyValue $v.Why -Force
|
||||
$mark = if ($v.Ready) { "OK " } else { "SKIP" }
|
||||
$col = if ($v.Ready) { "Green" } else { "Yellow" }
|
||||
Write-Host (" {0} {1,-14} #{2,-4} {3}" -f $mark, $p.Repo, $p.Number, $p.Title) -ForegroundColor $col
|
||||
if (-not $v.Ready) { Write-Host (" -> {0}" -f $v.Why) -ForegroundColor DarkYellow }
|
||||
}
|
||||
|
||||
$ready = @($queue | Where-Object { $_.Ready })
|
||||
Write-Host ""
|
||||
Write-Host "$($ready.Count) of $($queue.Count) ready to merge." -ForegroundColor Cyan
|
||||
|
||||
if (-not $Execute) {
|
||||
Write-Host ""
|
||||
Write-Host "Dry run. Re-run with -Execute (or -e) to merge." -ForegroundColor Yellow
|
||||
Write-ZTrailer
|
||||
exit 0
|
||||
}
|
||||
if ($ready.Count -eq 0) { Write-ZTrailer; exit 1 }
|
||||
|
||||
if (-not $Yes) {
|
||||
Write-Host ""
|
||||
$answer = Read-Host "Squash-merge these $($ready.Count) PRs and delete their branches? (y/N)"
|
||||
if ($answer -notmatch '^(y|yes)$') { Write-Host "Aborted." -ForegroundColor Yellow; Write-ZTrailer; exit 1 }
|
||||
}
|
||||
|
||||
$merged = @(); $failed = @()
|
||||
foreach ($p in $ready) {
|
||||
# Re-check: an earlier merge in this same repo may have conflicted this one.
|
||||
$v = Get-Readiness -RepoName $p.Repo -Number $p.Number
|
||||
if (-not $v.Ready) {
|
||||
Write-Host (" SKIP {0} #{1} - {2}" -f $p.Repo, $p.Number, $v.Why) -ForegroundColor Yellow
|
||||
$failed += [pscustomobject]@{ Repo = $p.Repo; Number = $p.Number; Why = $v.Why }
|
||||
continue
|
||||
}
|
||||
$r = Invoke-Gh @("pr", "merge", "$($p.Number)", "-R", "$ORG/$($p.Repo)",
|
||||
"--squash", "--delete-branch") -AllowFail
|
||||
if ($r.Code -eq 0) {
|
||||
Write-Host (" MERGED {0} #{1}" -f $p.Repo, $p.Number) -ForegroundColor Green
|
||||
$merged += $p
|
||||
} else {
|
||||
Write-Host (" FAILED {0} #{1}" -f $p.Repo, $p.Number) -ForegroundColor Red
|
||||
Write-Host (" {0}" -f $r.Output) -ForegroundColor DarkRed
|
||||
$failed += [pscustomobject]@{ Repo = $p.Repo; Number = $p.Number; Why = $r.Output }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "merged $($merged.Count), failed/skipped $($failed.Count)" -ForegroundColor Cyan
|
||||
|
||||
# Verify rather than trust the exit codes - a merge can report success and leave
|
||||
# the PR in an unexpected state.
|
||||
if ($merged.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "Verifying:" -ForegroundColor Cyan
|
||||
foreach ($p in $merged) {
|
||||
$r = Invoke-Gh @("pr", "view", "$($p.Number)", "-R", "$ORG/$($p.Repo)",
|
||||
"--json", "state,mergedAt") -AllowFail
|
||||
$j = if ($r.Code -eq 0) { $r.Output | ConvertFrom-Json } else { $null }
|
||||
$state = if ($j) { $j.state } else { "unreadable" }
|
||||
$col = if ($state -eq "MERGED") { "Green" } else { "Red" }
|
||||
Write-Host (" {0,-14} #{1,-4} {2}" -f $p.Repo, $p.Number, $state) -ForegroundColor $col
|
||||
}
|
||||
|
||||
# Follow-up, printed not run: ONE build bump per release - not one per
|
||||
# merged PR - on the default branch, and then a deploy. Both deliberately
|
||||
# by hand. This used to print one bump per PR, which is how a single
|
||||
# release came to be stamped as two builds.
|
||||
Write-Host ""
|
||||
Write-Host "Follow-up (not run):" -ForegroundColor Cyan
|
||||
foreach ($grp in ($merged | Group-Object Repo)) {
|
||||
$how = if ($BUMP.ContainsKey($grp.Name)) { $BUMP[$grp.Name] } else { "bump this repo's build stamp" }
|
||||
Write-Host (" {0,-14} {1} merged -> 1 build bump for the release: {2}" -f $grp.Name, $grp.Count, $how)
|
||||
}
|
||||
Write-Host " then deploy each project you want live (zdeploy, by hand)"
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) { Write-ZTrailer; exit 1 }
|
||||
|
||||
Write-ZTrailer
|
||||
6
zpull.cmd
Normal file
6
zpull.cmd
Normal file
@ -0,0 +1,6 @@
|
||||
@echo off
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.28
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zpull.ps1" %*
|
||||
359
zpull.ps1
Normal file
359
zpull.ps1
Normal file
@ -0,0 +1,359 @@
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zpull.ps1 - merge the fleet's ready PRs, then bring the local checkouts current.
|
||||
#
|
||||
# Usage:
|
||||
# zpull dry run: what would merge, what would pull
|
||||
# zpull -Execute merge ready PRs, then pull every affected checkout
|
||||
# zpull -e the same; -e is an alias, as -s is for -Scan
|
||||
# zpull -Repo <name> limit to one repo
|
||||
# zpull -Only 65 merge just these PR numbers
|
||||
# zpull -PullOnly skip merging; only bring checkouts up to date
|
||||
# zpull -Execute -Yes skip zmerge's confirmation prompt
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# ---------------
|
||||
# zmerge stops at the merge, deliberately - deploys are run by hand. But the
|
||||
# tooling repos are not deployed anywhere at all: they run
|
||||
# from the local checkout. For those, "deployed" just means "pulled". Merging a
|
||||
# zdeploy.ps1 fix and then forgetting the pull leaves you running the old file
|
||||
# while GitHub says the bug is fixed - which is its own kind of lie.
|
||||
#
|
||||
# So: merge (via zmerge, which owns all the mergeability safety), then pull.
|
||||
#
|
||||
# WHAT IT WILL NOT DO
|
||||
# -------------------
|
||||
# * pull over uncommitted work. It reports and skips. Twice this month a
|
||||
# checkout sat on a feature branch or held unstaged edits, and anything that
|
||||
# "helpfully" resolved that would have destroyed real work.
|
||||
# * pull anything but a fast-forward. A diverged local main is a decision,
|
||||
# not something a sync script should guess at.
|
||||
# * deploy to a server. Still by hand. This only touches local checkouts.
|
||||
#
|
||||
# HOW CHECKOUTS ARE FOUND
|
||||
# -----------------------
|
||||
# By reading each candidate directory's `origin` remote and matching the repo
|
||||
# name, not from a hardcoded table - a table drifts the moment a directory is
|
||||
# renamed, and this fleet renames directories.
|
||||
|
||||
[CmdletBinding(PositionalBinding = $false)]
|
||||
param(
|
||||
[Alias('e')][switch]$Execute,
|
||||
[switch]$Yes,
|
||||
[switch]$PullOnly,
|
||||
# Sweep only the repos with no zdeploy target - the ones where a pull is
|
||||
# the whole job. Tooling, archives, libraries.
|
||||
[switch]$ReposOnly,
|
||||
[string]$Repo,
|
||||
[int[]]$Only = @(),
|
||||
[int[]]$Exclude = @(),
|
||||
# PowerShell binds --help to -Help on its own (it tolerates the extra
|
||||
# dash), so this one switch answers --help, -help and -h. The bare words
|
||||
# land in $Rest below and are handled there.
|
||||
[Alias('h')][switch]$Help,
|
||||
# Catches anything unmatched. Without it, PositionalBinding=$false makes an
|
||||
# unknown argument a raw PowerShell binding error - a wall of red that does
|
||||
# not say what the valid arguments are. Owning the message means a typo
|
||||
# gets the usage block instead.
|
||||
[Parameter(ValueFromRemainingArguments = $true)][string[]]$Rest = @()
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Two blank lines at the end of a run, matching every other z-script, so output
|
||||
# is separated from the next prompt. Local copy rather than ZHelpers: this
|
||||
# script does not dot-source it.
|
||||
function Write-ZTrailer { Write-Host ""; Write-Host "" }
|
||||
|
||||
$FLEET_ROOT = Split-Path -Parent $PSScriptRoot
|
||||
$ORG = "evomedia-net"
|
||||
|
||||
function Show-ZPullUsage {
|
||||
Write-Host ""
|
||||
Write-Host "zpull - merge the fleet's ready PRs, then bring local checkouts current." -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Usage: zpull [-Execute|-e] [-Yes] [-PullOnly] [-ReposOnly] [-Repo <name>] [-Only <n,n>] [-Exclude <n,n>]" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host " (no args) dry run - what would merge, what would pull. Changes nothing." -ForegroundColor Gray
|
||||
Write-Host " -Execute, -e actually merge ready PRs, then pull every affected checkout" -ForegroundColor Gray
|
||||
Write-Host " -PullOnly skip merging entirely; only bring checkouts up to date" -ForegroundColor Gray
|
||||
Write-Host " -Repo <name> limit to one repo, by its GitHub name" -ForegroundColor Gray
|
||||
Write-Host " -Only <n,n> merge just these PR numbers" -ForegroundColor Gray
|
||||
Write-Host " -Exclude <n,n> merge everything ready except these PR numbers" -ForegroundColor Gray
|
||||
Write-Host " -ReposOnly only repos with no deploy target (pull = done)" -ForegroundColor Gray
|
||||
Write-Host " -Yes skip zmerge's confirmation prompt (needs -Execute)" -ForegroundColor Gray
|
||||
Write-Host " --help, -h this text" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "What each result line means:" -ForegroundColor Yellow
|
||||
Write-Host " ok already current - nothing to do" -ForegroundColor Gray
|
||||
Write-Host " PULLED fast-forwarded to the new tip" -ForegroundColor Gray
|
||||
Write-Host " SKIP deliberately left alone: uncommitted work, not on the default" -ForegroundColor Gray
|
||||
Write-Host " branch, or diverged. Never resolved automatically." -ForegroundColor Gray
|
||||
Write-Host " FAIL the repo could not be read or fetched. The sweep continues;" -ForegroundColor Gray
|
||||
Write-Host " that one repo is simply not current." -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "Each line is marked with what the repo still owes:" -ForegroundColor Yellow
|
||||
Write-Host " [repo] nothing runs from a server - the pull is the whole job" -ForegroundColor Gray
|
||||
Write-Host " [zdeploy <key>] a pull leaves the server on the old build" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
Write-Host "It will not pull over uncommitted work, will not do anything but a" -ForegroundColor DarkGray
|
||||
Write-Host "fast-forward, and will not deploy. Deploys stay manual." -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
Write-Host "Checkouts are found by reading each directory's origin remote under" -ForegroundColor DarkGray
|
||||
Write-Host "$FLEET_ROOT, not from a hardcoded list." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# Bare-word help too, matching the rest of the toolkit (zdeploy myapp, zkill all).
|
||||
$helpWords = @('help', '?', '/?', '--help', '-help')
|
||||
if ($Help -or @($Rest | Where-Object { $helpWords -contains $_.ToLowerInvariant() }).Count -gt 0) {
|
||||
Show-ZPullUsage
|
||||
Write-ZTrailer
|
||||
exit 0
|
||||
}
|
||||
if ($Rest.Count -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: unrecognised argument(s): $($Rest -join ', ')" -ForegroundColor Red
|
||||
Show-ZPullUsage
|
||||
Write-ZTrailer
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-DeployTargetsByPath {
|
||||
# Checkout path -> the zdeploy keys that ship from it.
|
||||
#
|
||||
# Read from zconfig rather than listed here: a second table would drift the
|
||||
# first time a target is added, and drift in THIS table is the failure it
|
||||
# exists to prevent - a repo quietly reported as "done at the pull" while a
|
||||
# server runs the old build.
|
||||
#
|
||||
# Matched by containment, not equality, because a target's localRoot is
|
||||
# often a subdirectory of its checkout (a service may ship from
|
||||
# <checkout>\<subdir>), and one checkout can carry several targets
|
||||
# (vidplayer ships cardiff, opensesame and kelly).
|
||||
$map = @{}
|
||||
# Read here rather than via ZHelpers' Get-ZConfig: this script is
|
||||
# standalone by design, and that helper exits the process when the config
|
||||
# is missing - which would turn "no zconfig" into a dead sweep instead of
|
||||
# a sweep that simply knows of no deploy targets.
|
||||
$configPath = if ($env:ZCONFIG) { $env:ZCONFIG } else { Join-Path $PSScriptRoot "zconfig.json" }
|
||||
if (-not (Test-Path -LiteralPath $configPath)) { return $map }
|
||||
try {
|
||||
$cfg = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-Host " (zconfig.json unreadable - every repo will report as [repo])" -ForegroundColor Yellow
|
||||
return $map
|
||||
}
|
||||
if (-not $cfg.projects) { return $map }
|
||||
foreach ($key in $cfg.projects.PSObject.Properties.Name) {
|
||||
if ($key -like '_*') { continue } # underscore keys are comments
|
||||
$root = $cfg.projects.$key.localRoot
|
||||
if (-not $root) { continue }
|
||||
try { $map[$key] = [System.IO.Path]::GetFullPath($root).TrimEnd('\') } catch { }
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
function Get-DeployKeysFor {
|
||||
param([string]$Path, [hashtable]$Targets)
|
||||
$full = [System.IO.Path]::GetFullPath($Path).TrimEnd('\')
|
||||
$hits = @()
|
||||
foreach ($key in $Targets.Keys) {
|
||||
$t = $Targets[$key]
|
||||
if ($t -eq $full -or $t.StartsWith($full + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$hits += $key
|
||||
}
|
||||
}
|
||||
return @($hits | Sort-Object)
|
||||
}
|
||||
|
||||
function Get-LocalCheckouts {
|
||||
# repo name -> local path, discovered from origin remotes.
|
||||
$map = @{}
|
||||
$candidates = @(Get-ChildItem -LiteralPath $FLEET_ROOT -Directory -ErrorAction SilentlyContinue)
|
||||
# One level deeper too: some projects keep theirs nested.
|
||||
foreach ($d in @($candidates)) {
|
||||
$candidates += @(Get-ChildItem -LiteralPath $d.FullName -Directory -ErrorAction SilentlyContinue)
|
||||
}
|
||||
foreach ($d in $candidates) {
|
||||
if (-not (Test-Path (Join-Path $d.FullName ".git"))) { continue }
|
||||
# A pruned worktree leaves a .git FILE pointing at an admin dir that no
|
||||
# longer exists, so Test-Path above passes and git then fails. Same
|
||||
# redirect trap as everywhere else, so keep this on Continue and judge
|
||||
# by exit code.
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
$url = (git -C $d.FullName remote get-url origin 2>$null)
|
||||
$ok = ($LASTEXITCODE -eq 0)
|
||||
$ErrorActionPreference = $prev
|
||||
if (-not $ok -or -not $url) { continue }
|
||||
if ($url -match "[:/]$ORG/([^/]+?)(\.git)?$") {
|
||||
$name = $Matches[1]
|
||||
if (-not $map.ContainsKey($name)) { $map[$name] = $d.FullName }
|
||||
}
|
||||
}
|
||||
return $map
|
||||
}
|
||||
|
||||
function Get-DefaultBranch {
|
||||
# origin/HEAD is a LOCAL cache of the remote's default branch. It is written
|
||||
# at clone time, and repos created some other way (git init + remote add,
|
||||
# which is how the *-stack and hostops checkouts here were made) simply do
|
||||
# not have it. `git symbolic-ref` then fails with
|
||||
# fatal: ref refs/remotes/origin/HEAD is not a symbolic ref
|
||||
# and - because this script runs under ErrorActionPreference='Stop' - PS 5.1
|
||||
# turns that redirected stderr into a TERMINATING NativeCommandError. The
|
||||
# 2>$null does not prevent it; it is the redirect itself that wraps each
|
||||
# stderr line in an ErrorRecord. So drop to Continue for the native calls.
|
||||
param([string]$Path)
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$d = (git -C $Path symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
|
||||
if (-not $d) {
|
||||
# Repair the cache from the remote, then re-ask. Costs one network
|
||||
# round-trip on first run per repo and is permanent afterwards.
|
||||
git -C $Path remote set-head origin --auto 2>$null | Out-Null
|
||||
$d = (git -C $Path symbolic-ref --short refs/remotes/origin/HEAD 2>$null) -replace '^origin/', ''
|
||||
}
|
||||
if (-not $d) {
|
||||
# Offline, or no such remote. Believe the remote-tracking refs that
|
||||
# exist rather than assuming "main" - zscripts is on master, and
|
||||
# guessing wrong makes this script skip the repo with a misleading
|
||||
# "on 'master', not 'main'".
|
||||
foreach ($c in @('main', 'master')) {
|
||||
git -C $Path rev-parse --verify --quiet "refs/remotes/origin/$c" 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $d = $c; break }
|
||||
}
|
||||
}
|
||||
if (-not $d) { $d = (git -C $Path rev-parse --abbrev-ref HEAD 2>$null) }
|
||||
if (-not $d) { $d = "main" }
|
||||
return $d
|
||||
}
|
||||
finally { $ErrorActionPreference = $prev }
|
||||
}
|
||||
|
||||
function Sync-Checkout {
|
||||
param([string]$Name, [string]$Path, [bool]$DoIt, [string[]]$DeployKeys = @())
|
||||
|
||||
# Everything below judges git by $LASTEXITCODE, so drop to Continue for the
|
||||
# whole function (scoped, auto-reverts on exit).
|
||||
#
|
||||
# This is not tidiness. Under the script's ErrorActionPreference='Stop', a
|
||||
# stderr REDIRECT on a native command makes PS 5.1 wrap each stderr line in
|
||||
# a terminating ErrorRecord - so `git fetch origin 2>$null` against one
|
||||
# repo with an unreachable remote killed the ENTIRE sweep mid-list, leaving
|
||||
# every repo after it unvisited and unreported. A fleet sweep must survive
|
||||
# one bad repo; that repo gets a FAIL row and the run continues.
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
Sync-CheckoutCore -Name $Name -Path $Path -DoIt $DoIt -DeployKeys $DeployKeys
|
||||
}
|
||||
catch {
|
||||
Write-Host (" {0,-18} FAIL {1}" -f $Name, $_.Exception.Message) -ForegroundColor Red
|
||||
}
|
||||
finally { $ErrorActionPreference = $prev }
|
||||
}
|
||||
|
||||
function Sync-CheckoutCore {
|
||||
param([string]$Name, [string]$Path, [bool]$DoIt, [string[]]$DeployKeys = @())
|
||||
|
||||
# Appended to every line: the point is that you never have to remember
|
||||
# which kind of repo you are looking at.
|
||||
$mark = if ($DeployKeys.Count -gt 0) { " [zdeploy $($DeployKeys -join ', ')]" } else { " [repo]" }
|
||||
|
||||
$branch = (git -C $Path rev-parse --abbrev-ref HEAD 2>$null)
|
||||
if ($LASTEXITCODE -ne 0 -or -not $branch) {
|
||||
Write-Host (" {0,-18} FAIL not a usable git checkout: {1}{2}" -f $Name, $Path, $mark) -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
$dirty = @(git -C $Path status --porcelain --untracked-files=no 2>$null)
|
||||
$default = Get-DefaultBranch -Path $Path
|
||||
|
||||
if ($dirty) {
|
||||
Write-Host (" {0,-18} SKIP uncommitted changes ({1} file(s)) - commit or stash first{2}" -f $Name, $dirty.Count, $mark) -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
if ($branch -ne $default) {
|
||||
Write-Host (" {0,-18} SKIP on '{1}', not '{2}'{3}" -f $Name, $branch, $default, $mark) -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
|
||||
git -C $Path fetch origin --quiet 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
# Unreachable remote, renamed repo, dead credential. Say so and move on
|
||||
# - continuing would compare against stale remote-tracking refs and
|
||||
# report "already current" about a repo we could not actually reach.
|
||||
Write-Host (" {0,-18} FAIL cannot fetch origin - check the remote{1}" -f $Name, $mark) -ForegroundColor Red
|
||||
return
|
||||
}
|
||||
$behind = (git -C $Path rev-list --count "HEAD..origin/$default" 2>$null)
|
||||
$ahead = (git -C $Path rev-list --count "origin/$default..HEAD" 2>$null)
|
||||
|
||||
if ([int]$ahead -gt 0) {
|
||||
Write-Host (" {0,-18} SKIP local '{1}' is {2} commit(s) ahead - diverged, resolve by hand{3}" -f $Name, $default, $ahead, $mark) -ForegroundColor Yellow
|
||||
return
|
||||
}
|
||||
if ([int]$behind -eq 0) {
|
||||
Write-Host (" {0,-18} ok already current{1}" -f $Name, $mark) -ForegroundColor DarkGray
|
||||
return
|
||||
}
|
||||
if (-not $DoIt) {
|
||||
Write-Host (" {0,-18} would pull {1} commit(s){2}" -f $Name, $behind, $mark) -ForegroundColor Cyan
|
||||
return
|
||||
}
|
||||
git -C $Path merge --ff-only "origin/$default" --quiet 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host (" {0,-18} PULLED {1} commit(s) -> {2}{3}" -f $Name, $behind, (git -C $Path rev-parse --short HEAD), $mark) -ForegroundColor Green
|
||||
# The whole reason the marker exists. A tooling repo is finished here;
|
||||
# a deployable one now has a checkout ahead of its own server, which is
|
||||
# the state that gets forgotten.
|
||||
foreach ($k in $DeployKeys) {
|
||||
Write-Host (" {0,-18} still on the old build - run: zdeploy {1}" -f "", $k) -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host (" {0,-18} FAILED to fast-forward{1}" -f $Name, $mark) -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
# ── 1. Merge ─────────────────────────────────────────────────────
|
||||
if (-not $PullOnly) {
|
||||
Write-Host "`n=== Merging ready PRs (via zmerge) ===" -ForegroundColor Cyan
|
||||
# Hashtable splatting, not an array. Array splatting passes elements
|
||||
# positionally, so @("-Repo","<name>") fed "-Repo" into zmerge's
|
||||
# [int[]]$Exclude and died on the type conversion.
|
||||
$zm = @{}
|
||||
if ($Execute) { $zm.Execute = $true }
|
||||
if ($Yes) { $zm.Yes = $true }
|
||||
if ($Repo) { $zm.Repo = $Repo }
|
||||
if ($Only) { $zm.Only = $Only }
|
||||
if ($Exclude) { $zm.Exclude = $Exclude }
|
||||
& (Join-Path $PSScriptRoot "zmerge.ps1") @zm
|
||||
}
|
||||
|
||||
# ── 2. Pull ──────────────────────────────────────────────────────
|
||||
Write-Host "`n=== Bringing local checkouts current ===" -ForegroundColor Cyan
|
||||
if (-not $Execute) {
|
||||
Write-Host " (dry run - nothing will be pulled; add -Execute or -e)" -ForegroundColor DarkGray
|
||||
}
|
||||
$checkouts = Get-LocalCheckouts
|
||||
if ($Repo) {
|
||||
if ($checkouts.ContainsKey($Repo)) { $checkouts = @{ $Repo = $checkouts[$Repo] } }
|
||||
else { Write-Host " no local checkout found for '$Repo'" -ForegroundColor Yellow; $checkouts = @{} }
|
||||
}
|
||||
$targets = Get-DeployTargetsByPath
|
||||
if ($ReposOnly) {
|
||||
Write-Host " (-ReposOnly: repos with a zdeploy target are not listed)" -ForegroundColor DarkGray
|
||||
}
|
||||
$shown = 0
|
||||
foreach ($name in ($checkouts.Keys | Sort-Object)) {
|
||||
$keys = Get-DeployKeysFor -Path $checkouts[$name] -Targets $targets
|
||||
if ($ReposOnly -and $keys.Count -gt 0) { continue }
|
||||
$shown++
|
||||
Sync-Checkout -Name $name -Path $checkouts[$name] -DoIt:$Execute -DeployKeys $keys
|
||||
}
|
||||
if ($shown -eq 0) { Write-Host " nothing matched" -ForegroundColor DarkGray }
|
||||
Write-ZTrailer
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zrelease.ps1 - package the current version as a downloadable zip.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrepair.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zrepair.ps1 — audit and repair container/proxy routing on the server, then smoke test.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through.
|
||||
& (Join-Path $PSScriptRoot "ZKiller.ps1") @args
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" -Detached %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsetup.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zsetup.ps1 — prepare a project for local dev: create its Python venv and install
|
||||
# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent -
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zsetup_mail.ps1 — create admin@ and noreply@ mailboxes in a docker-mailserver
|
||||
# container on the server, and print the DNS records + SMTP/IMAP settings to use.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" %*
|
||||
|
||||
28
zstart.ps1
28
zstart.ps1
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zstart.ps1 — start local dev servers for any project defined in zconfig.json.
|
||||
#
|
||||
@ -198,21 +198,15 @@ function Invoke-ProjectStartPrep {
|
||||
Write-Host " env $($item.Name)=$($item.Value)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
if ($Proj.start.gitPull -and (Test-Path (Join-Path $Proj.localRoot ".git"))) {
|
||||
Push-Location -LiteralPath $Proj.localRoot
|
||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||
# instead of blocking the server start on a "Username for ..." prompt.
|
||||
$prev = $env:GIT_TERMINAL_PROMPT; $env:GIT_TERMINAL_PROMPT = "0"
|
||||
try {
|
||||
$pullOut = git pull --ff-only 2>&1
|
||||
$last = ($pullOut | Select-Object -Last 1)
|
||||
Write-Host " git pull: $last" -ForegroundColor DarkGray
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Auto-pull skipped - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)" -ForegroundColor Yellow
|
||||
}
|
||||
} finally {
|
||||
$env:GIT_TERMINAL_PROMPT = $prev
|
||||
Pop-Location
|
||||
if ($Proj.start.gitPull) {
|
||||
# Never lets a pull stand between the user and a running server: the
|
||||
# helper reports, it does not throw (#130). The inline version this
|
||||
# replaced aborted on git's ordinary stderr progress under Stop.
|
||||
$pull = Invoke-StartGitPull -Root $Proj.localRoot
|
||||
if ($pull.Ok) {
|
||||
Write-Host " git pull: $($pull.Message)" -ForegroundColor DarkGray
|
||||
} else {
|
||||
Write-Host " Auto-pull skipped - starting with the current checkout. ($($pull.Message); set start.gitPull=false to stop trying)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zstart_docker.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zstart_docker.ps1 — bring up a local docker compose stack from <scriptsRoot>\docker\.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" -Detached %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zstop.ps1 — stop docker compose stacks on the server without removing data or files.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsync.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zsync.ps1 — copy new backup files offsite; or build + mirror a vite project's dist.
|
||||
#
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
REM Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
REM Created by Kelly Michels · dev@evomedia.net
|
||||
REM Licensed under the MIT License. See LICENSE.
|
||||
REM Version: v1.0.0.0.23
|
||||
REM Version: v1.0.0.0.28
|
||||
|
||||
@echo off
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
# evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||
# Created by Kelly Michels · dev@evomedia.net
|
||||
# Licensed under the MIT License. See LICENSE.
|
||||
# Version: v1.0.0.0.23
|
||||
# Version: v1.0.0.0.28
|
||||
|
||||
# zversion.ps1 - manage the toolkit version.
|
||||
#
|
||||
|
||||
Loading…
Reference in New Issue
Block a user