mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-08 07:28:18 +00:00
Compare commits
31 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 | |||
| f1b1fd6264 | |||
| 135c585c4b | |||
| 560a2064a2 | |||
| 40fa250a37 | |||
| af929f73ba | |||
| ac95c47255 |
14
.gitattributes
vendored
14
.gitattributes
vendored
@ -4,8 +4,18 @@
|
|||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as
|
# Every .txt here is either a generated twin or a manifest, and all of them
|
||||||
# part of the filename and reports every entry as missing.
|
# 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
|
CHECKSUMS.txt text eol=lf
|
||||||
releases/*.sha256 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)
|
# Pester coverage output (regenerated; never committed)
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
# Generated by scripts/plaintext_twins.py
|
||||||
|
__pycache__/
|
||||||
|
|||||||
213
CHANGELOG.md
213
CHANGELOG.md
@ -1,15 +1,182 @@
|
|||||||
<!--
|
<!--
|
||||||
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
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
Licensed under the MIT License. See LICENSE.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
Notable changes to the Evomedia.net Token Savers.
|
Notable changes to the evomedia.net Token Savers.
|
||||||
|
|
||||||
## Unreleased
|
## 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
|
||||||
|
- **Every release since 1.0.0 has its own section again** - 21 entries had
|
||||||
|
piled up under `Unreleased` while 22 builds shipped, so this file said
|
||||||
|
nothing had been released since 1.0.0 and a reader at any tag found no
|
||||||
|
section for the version they were holding. Which release carried which
|
||||||
|
entry was derived from git, not guessed: for every line, the commit that
|
||||||
|
introduced it, then the earliest tag containing that commit. No entry text
|
||||||
|
changed - only headings were added and whole entries moved under the
|
||||||
|
release that carried them.
|
||||||
|
- **`scripts/readme_txt.py` is now `scripts/plaintext_twins.py` and covers
|
||||||
|
every markdown file that owes a twin**, `CHANGELOG.txt` included. It was
|
||||||
|
kept by hand, so it drifted the moment this file was reorganised. The
|
||||||
|
renderer also drops `<!-- -->` markers, which are invisible in markdown
|
||||||
|
and read as stray punctuation in a text file.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The `--check` that keeps the twins honest is actually run now** -
|
||||||
|
`readme_txt.py` shipped one and its docstring claimed "the test suite runs
|
||||||
|
--check", but nothing invoked it, so a twin could disagree with its
|
||||||
|
markdown indefinitely. `tests/PlainTextTwins.Tests.ps1` runs it, and
|
||||||
|
reports *inconclusive* rather than passing when python is unavailable.
|
||||||
|
|
||||||
|
## v1.0.0.0.22 - 2026-08-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **The sanitization denylist moved to `tests/sanitization-patterns.psd1`**,
|
||||||
|
so the test suite here and the publisher in the private toolkit read one
|
||||||
|
list instead of keeping two. They had two, and they disagreed: the
|
||||||
|
publisher's scan looked only for secrets, while these rules are about
|
||||||
|
identity — internal project names, product domains, private-only script
|
||||||
|
names, operator paths. It therefore reported "clean" on files this suite
|
||||||
|
rejects. No rule changed; only where they live.
|
||||||
|
|
||||||
|
## v1.0.0.0.21 - 2026-08-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`tests/VerifyPlan.Tests.ps1`** — the verification channel-selection rules
|
||||||
|
are pure functions in `ZHelpers.ps1` (`Get-VerifyAttempts`,
|
||||||
|
`Get-VerifyTimeout`) and Pester pins them, including "a project with no
|
||||||
|
domain must never produce an edge attempt" and the PowerShell 5.1
|
||||||
|
one-element-unroll trap.
|
||||||
|
|
||||||
|
- **`scripts/readme_txt.py` and `README.txt`** — a generated plain-text twin
|
||||||
|
of the README for terminals and pagers. `README.txt` is generated, never
|
||||||
|
edited by hand.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **`zdeploy` verification picks its channel on every retry, and never asks
|
||||||
|
the bare IP** — the check used to choose its channel once, before the wait
|
||||||
|
loop, by probing; the probes raced the app restart the check exists to wait
|
||||||
|
through, so the whole window went to the edge fallback. For a project with
|
||||||
|
no `domain` that fallback had no `Host` header, and the proxy can then only
|
||||||
|
answer from its **default vhost — a different product**: that is how one
|
||||||
|
deploy's check compared another app's build number against its own. Now
|
||||||
|
channels are re-resolved each retry in trust order (docker-network
|
||||||
|
`viaProxy` → `localhost:<port>` → edge with the project's `Host`), the edge
|
||||||
|
is skipped entirely when there is no host to route by, and a project with
|
||||||
|
no trustworthy channel is reported as unverifiable instead of guessed at.
|
||||||
|
The final warning also says which failure happened: a version that never
|
||||||
|
matched (stale/failed build) reads differently from channels that never
|
||||||
|
answered (probably still booting). `verify.timeoutSeconds` joins the
|
||||||
|
config so a project that is slow to boot — e.g. one that runs database
|
||||||
|
migrations in its entrypoint — can widen its own window instead of
|
||||||
|
warning on every routine success.
|
||||||
|
|
||||||
|
- **An interrupted deploy can no longer destroy server-side `.env` files** —
|
||||||
|
the preserve/restore of operator files is transactional: the restore comes
|
||||||
|
from a tarball taken before the tree is replaced, so a deploy that dies
|
||||||
|
mid-flight leaves the previous files in place instead of an empty
|
||||||
|
directory.
|
||||||
|
|
||||||
|
- **A successful deploy no longer reports failure** — `docker compose
|
||||||
|
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
||||||
|
into a terminating error under `$ErrorActionPreference = 'Stop'`; four ssh
|
||||||
|
calls bypassed the wrapper that flattens this. All remote steps now run
|
||||||
|
through it and are judged by exit code alone.
|
||||||
|
|
||||||
|
## v1.0.0.0.20 - 2026-08-30
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **`zversion bump` is once per *release*, not once per PR** — the usage text
|
- **`zversion bump` is once per *release*, not once per PR** — the usage text
|
||||||
and the `bump` help line both said "one per PR, one per defect fix". The
|
and the `bump` help line both said "one per PR, one per defect fix". The
|
||||||
@ -20,6 +187,8 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
records what the rule was when `zversion` shipped, is deliberately left as
|
records what the rule was when `zversion` shipped, is deliberately left as
|
||||||
written.
|
written.
|
||||||
|
|
||||||
|
## v1.0.0.0.19 - 2026-08-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Read a live build from inside the docker network, not through the public
|
- **Read a live build from inside the docker network, not through the public
|
||||||
proxy** — `zdeploy`, `zec2` and `zec2online` now prefer
|
proxy** — `zdeploy`, `zec2` and `zec2online` now prefer
|
||||||
@ -39,6 +208,7 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
knows a version. Purely additive: projects without those two keys behave
|
knows a version. Purely additive: projects without those two keys behave
|
||||||
exactly as before.
|
exactly as before.
|
||||||
|
|
||||||
|
## v1.0.0.0.14 - 2026-08-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **`scp` no longer receives an ssh-only flag** — the stdin-hang fix added
|
- **`scp` no longer receives an ssh-only flag** — the stdin-hang fix added
|
||||||
@ -53,6 +223,8 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
without checking; it now points at `scp`'s own output, where the real
|
without checking; it now points at `scp`'s own output, where the real
|
||||||
diagnosis already was.
|
diagnosis already was.
|
||||||
|
|
||||||
|
## v1.0.0.0.8 - 2026-08-12
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- **Deploys can no longer hang forever on an ssh prompt** — every
|
- **Deploys can no longer hang forever on an ssh prompt** — every
|
||||||
deploy-path `ssh`/`scp` now carries `BatchMode=yes` plus connect and
|
deploy-path `ssh`/`scp` now carries `BatchMode=yes` plus connect and
|
||||||
@ -64,16 +236,19 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
there is no prompt on this path worth answering. `ServerAlive*` bounds a
|
there is no prompt on this path worth answering. `ServerAlive*` bounds a
|
||||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||||
host) to about a minute instead of hanging.
|
host) to about a minute instead of hanging.
|
||||||
|
|
||||||
- **Vendored archives survive the archive filter** — files under a
|
- **Vendored archives survive the archive filter** — files under a
|
||||||
`vendor/` directory are exempt from the "no archives in the zip" rule.
|
`vendor/` directory are exempt from the "no archives in the zip" rule.
|
||||||
A project that vendors a dependency as `vendor/*.tgz` needs it in the
|
A project that vendors a dependency as `vendor/*.tgz` needs it in the
|
||||||
deploy zip; dropping it makes a Dockerfile's `COPY vendor ./vendor`
|
deploy zip; dropping it makes a Dockerfile's `COPY vendor ./vendor`
|
||||||
fail at image build, a confusing way to learn the filter ate a build
|
fail at image build, a confusing way to learn the filter ate a build
|
||||||
input.
|
input.
|
||||||
|
|
||||||
- **`unzip` install is idempotent** — the remote step ran
|
- **`unzip` install is idempotent** — the remote step ran
|
||||||
`apt-get update && apt-get install -y unzip` on every deploy; it now
|
`apt-get update && apt-get install -y unzip` on every deploy; it now
|
||||||
checks `command -v unzip` first and skips the apt round-trip when the
|
checks `command -v unzip` first and skips the apt round-trip when the
|
||||||
binary is already there.
|
binary is already there.
|
||||||
|
|
||||||
- **`zdeploy` edge kind now ships asset subdirectories** (#42) — the edge
|
- **`zdeploy` edge kind now ships asset subdirectories** (#42) — the edge
|
||||||
deploy uploaded top-level files only, so a project self-hosting assets
|
deploy uploaded top-level files only, so a project self-hosting assets
|
||||||
in folders (`fonts/`, `vendor/`) lost them on every deploy: docker
|
in folders (`fonts/`, `vendor/`) lost them on every deploy: docker
|
||||||
@ -82,15 +257,8 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
loading. Every subdirectory except `nginx-logs/` and `.git/` now ships
|
loading. Every subdirectory except `nginx-logs/` and `.git/` now ships
|
||||||
recursively, and the `ensure edge dir` chown is recursive so scp into
|
recursively, and the `ensure edge dir` chown is recursive so scp into
|
||||||
docker-created root-owned dirs cannot fail.
|
docker-created root-owned dirs cannot fail.
|
||||||
- **`zdeploy` no longer deletes operator-managed files on deploy** (#2) —
|
|
||||||
the project-directory replacement preserved only `./.env`, silently
|
## v1.0.0.0.0 - 2026-07-28
|
||||||
destroying every other server-side file (`.env.db`, staged signing
|
|
||||||
keys, certs) on every deploy. All `.env*` files at the project root are
|
|
||||||
now preserved by default, plus anything listed in the new
|
|
||||||
`deploy.preserve` array (files or directories); the vite kind, which
|
|
||||||
previously preserved nothing, gets the same protection. Found the hard
|
|
||||||
way: a first production deploy of an auth service wiped its staged DB
|
|
||||||
credentials and RSA signing keys.
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
||||||
@ -104,6 +272,7 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
hash verifies the download, the bundled `CHECKSUMS.txt` verifies the
|
hash verifies the download, the bundled `CHECKSUMS.txt` verifies the
|
||||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||||
copy. Released zips are immutable — `zrelease` refuses to overwrite one.
|
copy. Released zips are immutable — `zrelease` refuses to overwrite one.
|
||||||
|
|
||||||
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
- **`zchecksums` + `CHECKSUMS.txt`** — a SHA-256 manifest covering every `.ps1`
|
||||||
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
and `.cmd`, so a download can be verified before anything is run. `zchecksums`
|
||||||
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
checks them; `zchecksums -Update` regenerates after an intentional edit. The
|
||||||
@ -114,15 +283,18 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
It's an integrity check, not a signature — the manifest sits in the same repo
|
||||||
as the code, so it catches corruption and accidental drift, not a compromised
|
as the code, so it catches corruption and accidental drift, not a compromised
|
||||||
repo. A Pester test fails if the manifest ever goes stale.
|
repo. A Pester test fails if the manifest ever goes stale.
|
||||||
|
|
||||||
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
- **Test suite (Pester)** — the toolkit now has automated coverage of its own
|
||||||
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
pure logic: `Get-ArchiveExcludes` (including the deploy-vs-backup rule that
|
||||||
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
keeps `.env`/`uploads` out of deploys but *in* backups), config and project
|
||||||
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
lookups, `remote.composeDir` fallback, EC2 target composition, and build-label
|
||||||
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
formatting. Run with `Invoke-Pester .\tests` (Pester 5+). Verified by mutation
|
||||||
testing — reintroducing each historical bug turns the suite red.
|
testing — reintroducing each historical bug turns the suite red.
|
||||||
|
|
||||||
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
- **`ZCONFIG` environment variable** — overrides the path to `zconfig.json`, so
|
||||||
a run can target an alternate config. Also gives the test suite a seam for
|
a run can target an alternate config. Also gives the test suite a seam for
|
||||||
injecting a fixture.
|
injecting a fixture.
|
||||||
|
|
||||||
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
- **`zec2_rotatekeys` — safely rotate/reset server-side secrets** — a new
|
||||||
tool for when a secret leaks or a deploy overwrites a production `.env`
|
tool for when a secret leaks or a deploy overwrites a production `.env`
|
||||||
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
with dev values. `-Rotate KEY` regenerates a key **on the server**
|
||||||
@ -135,10 +307,12 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
the container (`up -d --force-recreate`, so the new values actually load —
|
the container (`up -d --force-recreate`, so the new values actually load —
|
||||||
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
a plain restart keeps the old environment). `-WhatIf` previews the plan
|
||||||
without touching anything.
|
without touching anything.
|
||||||
|
|
||||||
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
- **`zkill all`** — `zkill` now accepts `all`, stopping the dev server of
|
||||||
every project that has a `ports.dev` (edge/docker stacks with no local dev
|
every project that has a `ports.dev` (edge/docker stacks with no local dev
|
||||||
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
server are skipped). Brings it in line with `zdeploy all` / `zbackup all`;
|
||||||
the one-shot "stop everything I've got running locally".
|
the one-shot "stop everything I've got running locally".
|
||||||
|
|
||||||
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
- **`zdeploy` server-side health verification (`verify` block)** — projects
|
||||||
not published through the edge proxy can declare
|
not published through the edge proxy can declare
|
||||||
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
`"verify": { "port": ..., "path": "/health", "expect": "..." }` and the
|
||||||
@ -146,16 +320,19 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
||||||
proxy's default vhost answered for apps that never started; projects
|
proxy's default vhost answered for apps that never started; projects
|
||||||
with neither `domain` nor `verify` are now reported as NOT verified.
|
with neither `domain` nor `verify` are now reported as NOT verified.
|
||||||
|
|
||||||
- **`zdeploy` optional `deploy.gitPull`** — `git pull --ff-only` in the
|
- **`zdeploy` optional `deploy.gitPull`** — `git pull --ff-only` in the
|
||||||
project root before zipping. `zdeploy` zips the working tree and doesn't
|
project root before zipping. `zdeploy` zips the working tree and doesn't
|
||||||
otherwise pull, so a checkout left behind `origin` after a merged PR would
|
otherwise pull, so a checkout left behind `origin` after a merged PR would
|
||||||
deploy stale code while still bumping the build number — success that
|
deploy stale code while still bumping the build number — success that
|
||||||
changes nothing. A failed pull aborts the deploy instead.
|
changes nothing. A failed pull aborts the deploy instead.
|
||||||
|
|
||||||
- **Per-project `start` config block** — `zstart` honors optional pre-start
|
- **Per-project `start` config block** — `zstart` honors optional pre-start
|
||||||
steps from `zconfig.json`: `"gitPull": true` runs `git pull --ff-only` in
|
steps from `zconfig.json`: `"gitPull": true` runs `git pull --ff-only` in
|
||||||
the project root before starting (never boot a stale checkout), and
|
the project root before starting (never boot a stale checkout), and
|
||||||
`"env": { ... }` sets environment variables for the dev-server process.
|
`"env": { ... }` sets environment variables for the dev-server process.
|
||||||
Example added to `zconfig.example.json`.
|
Example added to `zconfig.example.json`.
|
||||||
|
|
||||||
- **Switch-style argument tolerance** — a leading dash on a project key is
|
- **Switch-style argument tolerance** — a leading dash on a project key is
|
||||||
ignored everywhere (`zdeploy -myapp` == `zdeploy myapp`), for hands that
|
ignored everywhere (`zdeploy -myapp` == `zdeploy myapp`), for hands that
|
||||||
grew up on per-project switches.
|
grew up on per-project switches.
|
||||||
@ -166,19 +343,33 @@ Notable changes to the Evomedia.net Token Savers.
|
|||||||
`all` does what bare invocation used to (matching `zdeploy`). The
|
`all` does what bare invocation used to (matching `zdeploy`). The
|
||||||
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
scheduled task created by `setup_backup_schedule.ps1` passes `all` —
|
||||||
re-run it if your task was registered before this change.
|
re-run it if your task was registered before this change.
|
||||||
|
|
||||||
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
- **`zbackup` parses more `DATABASE_URL` styles** — double/single-quoted
|
||||||
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
values (Prisma convention), `postgres://` and `postgresql+driver://`
|
||||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||||
previously these skipped the Postgres dump with "Could not parse
|
previously these skipped the Postgres dump with "Could not parse
|
||||||
DATABASE_URL".
|
DATABASE_URL".
|
||||||
|
|
||||||
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
- **`zkill` / port cleanup kills the whole process tree** — listeners on a
|
||||||
project's port are now terminated children-first. Auto-reloading servers
|
project's port are now terminated children-first. Auto-reloading servers
|
||||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||||
socket; killing only the parent left orphans serving stale code.
|
socket; killing only the parent left orphans serving stale code.
|
||||||
|
|
||||||
- **`zbackup` finds `DATABASE_URL` in `backend\.env` too** — projects with a
|
- **`zbackup` finds `DATABASE_URL` in `backend\.env` too** — projects with a
|
||||||
frontend/backend split get their Postgres dump bundled without needing a
|
frontend/backend split get their Postgres dump bundled without needing a
|
||||||
root-level `.env`.
|
root-level `.env`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`zdeploy` no longer deletes operator-managed files on deploy** (#2) —
|
||||||
|
the project-directory replacement preserved only `./.env`, silently
|
||||||
|
destroying every other server-side file (`.env.db`, staged signing
|
||||||
|
keys, certs) on every deploy. All `.env*` files at the project root are
|
||||||
|
now preserved by default, plus anything listed in the new
|
||||||
|
`deploy.preserve` array (files or directories); the vite kind, which
|
||||||
|
previously preserved nothing, gets the same protection. Found the hard
|
||||||
|
way: a first production deploy of an auth service wiped its staged DB
|
||||||
|
credentials and RSA signing keys.
|
||||||
|
|
||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
Initial public release: `zstart` / `zkill` / `zrestart` (local dev servers),
|
Initial public release: `zstart` / `zkill` / `zrestart` (local dev servers),
|
||||||
|
|||||||
280
CHANGELOG.txt
280
CHANGELOG.txt
@ -1,43 +1,240 @@
|
|||||||
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
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Notable changes to the Evomedia.net Token Savers.
|
Notable changes to the evomedia.net Token Savers.
|
||||||
|
|
||||||
Unreleased
|
Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
v1.0.0.0.28 - 2026-09-22
|
||||||
|
------------------------
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
- zversion bump is once per RELEASE, not once per PR - the usage text and
|
-------
|
||||||
the bump help line both said "one per PR, one per defect fix". The build
|
|
||||||
number names something that shipped, so a release carrying five PRs moves
|
- **zec2 and zec2online comments now say what they mean without
|
||||||
it by one; PRs that never shipped on their own were never separate builds.
|
naming private detail.** The container-side version read is described
|
||||||
Help text only here, but it is the wording people follow: it stamped a
|
by what it is - an endpoint that is not public on every project - rather
|
||||||
single evo.www release as two builds. The historical entry below, which
|
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
|
||||||
|
-------
|
||||||
|
- Every release since 1.0.0 has its own section again - 21 entries had
|
||||||
|
piled up under Unreleased while 22 builds shipped, so this file said
|
||||||
|
nothing had been released since 1.0.0 and a reader at any tag found no
|
||||||
|
section for the version they were holding. Which release carried which
|
||||||
|
entry was derived from git, not guessed: for every line, the commit that
|
||||||
|
introduced it, then the earliest tag containing that commit. No entry text
|
||||||
|
changed - only headings were added and whole entries moved under the
|
||||||
|
release that carried them.
|
||||||
|
- **scripts/readme_txt.py is now scripts/plaintext_twins.py and covers
|
||||||
|
every markdown file that owes a twin**, CHANGELOG.txt included. It was
|
||||||
|
kept by hand, so it drifted the moment this file was reorganised. The
|
||||||
|
renderer also drops <!-- --> markers, which are invisible in markdown
|
||||||
|
and read as stray punctuation in a text file.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
-----
|
||||||
|
- The --check that keeps the twins honest is actually run now -
|
||||||
|
readme_txt.py shipped one and its docstring claimed "the test suite runs
|
||||||
|
--check", but nothing invoked it, so a twin could disagree with its
|
||||||
|
markdown indefinitely. tests/PlainTextTwins.Tests.ps1 runs it, and
|
||||||
|
reports inconclusive rather than passing when python is unavailable.
|
||||||
|
|
||||||
|
v1.0.0.0.22 - 2026-08-31
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Changed
|
||||||
|
-------
|
||||||
|
- The sanitization denylist moved to tests/sanitization-patterns.psd1,
|
||||||
|
so the test suite here and the publisher in the private toolkit read one
|
||||||
|
list instead of keeping two. They had two, and they disagreed: the
|
||||||
|
publisher's scan looked only for secrets, while these rules are about
|
||||||
|
identity — internal project names, product domains, private-only script
|
||||||
|
names, operator paths. It therefore reported "clean" on files this suite
|
||||||
|
rejects. No rule changed; only where they live.
|
||||||
|
|
||||||
|
v1.0.0.0.21 - 2026-08-31
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Added
|
||||||
|
-----
|
||||||
|
- tests/VerifyPlan.Tests.ps1 — the verification channel-selection rules
|
||||||
|
are pure functions in ZHelpers.ps1 (Get-VerifyAttempts,
|
||||||
|
Get-VerifyTimeout) and Pester pins them, including "a project with no
|
||||||
|
domain must never produce an edge attempt" and the PowerShell 5.1
|
||||||
|
one-element-unroll trap.
|
||||||
|
|
||||||
|
- scripts/readme_txt.py and README.txt — a generated plain-text twin
|
||||||
|
of the README for terminals and pagers. README.txt is generated, never
|
||||||
|
edited by hand.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
-------
|
||||||
|
- **zdeploy verification picks its channel on every retry, and never asks
|
||||||
|
the bare IP** — the check used to choose its channel once, before the wait
|
||||||
|
loop, by probing; the probes raced the app restart the check exists to wait
|
||||||
|
through, so the whole window went to the edge fallback. For a project with
|
||||||
|
no domain that fallback had no Host header, and the proxy can then only
|
||||||
|
answer from its default vhost — a different product: that is how one
|
||||||
|
deploy's check compared another app's build number against its own. Now
|
||||||
|
channels are re-resolved each retry in trust order (docker-network
|
||||||
|
viaProxy → localhost:<port> → edge with the project's Host), the edge
|
||||||
|
is skipped entirely when there is no host to route by, and a project with
|
||||||
|
no trustworthy channel is reported as unverifiable instead of guessed at.
|
||||||
|
The final warning also says which failure happened: a version that never
|
||||||
|
matched (stale/failed build) reads differently from channels that never
|
||||||
|
answered (probably still booting). verify.timeoutSeconds joins the
|
||||||
|
config so a project that is slow to boot — e.g. one that runs database
|
||||||
|
migrations in its entrypoint — can widen its own window instead of
|
||||||
|
warning on every routine success.
|
||||||
|
|
||||||
|
- An interrupted deploy can no longer destroy server-side .env files —
|
||||||
|
the preserve/restore of operator files is transactional: the restore comes
|
||||||
|
from a tarball taken before the tree is replaced, so a deploy that dies
|
||||||
|
mid-flight leaves the previous files in place instead of an empty
|
||||||
|
directory.
|
||||||
|
|
||||||
|
- A successful deploy no longer reports failure — `docker compose
|
||||||
|
restart` writes routine progress to stderr, which PowerShell 5.1 turns
|
||||||
|
into a terminating error under $ErrorActionPreference = 'Stop'; four ssh
|
||||||
|
calls bypassed the wrapper that flattens this. All remote steps now run
|
||||||
|
through it and are judged by exit code alone.
|
||||||
|
|
||||||
|
v1.0.0.0.20 - 2026-08-30
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Changed
|
||||||
|
-------
|
||||||
|
- **zversion bump is once per release, not once per PR** — the usage text
|
||||||
|
and the bump help line both said "one per PR, one per defect fix". The
|
||||||
|
build number names something that shipped, so a release carrying five PRs
|
||||||
|
moves it by one; PRs that never shipped on their own were never separate
|
||||||
|
builds. Help text only here, but it is the wording people follow: it stamped
|
||||||
|
a single evo.www release as two builds. The historical entry below, which
|
||||||
records what the rule was when zversion shipped, is deliberately left as
|
records what the rule was when zversion shipped, is deliberately left as
|
||||||
written.
|
written.
|
||||||
|
|
||||||
|
v1.0.0.0.19 - 2026-08-30
|
||||||
|
------------------------
|
||||||
|
|
||||||
Added
|
Added
|
||||||
- Read a live build from inside the docker network, not through the public
|
-----
|
||||||
proxy — zdeploy, zec2 and zec2online now prefer
|
- **Read a live build from inside the docker network, not through the public
|
||||||
|
proxy** — zdeploy, zec2 and zec2online now prefer
|
||||||
docker exec <viaProxy> curl http://<upstream>/api/build-version when a
|
docker exec <viaProxy> curl http://<upstream>/api/build-version when a
|
||||||
project sets verify.viaProxy and verify.upstream.
|
project sets verify.viaProxy and verify.upstream.
|
||||||
Two problems it closes. A build stamp is something many sites
|
|
||||||
deliberately do not serve publicly, and a checker that reads it over the
|
|
||||||
public URL stops working the moment that endpoint is blocked —
|
|
||||||
reporting "unknown", which is indistinguishable from "could not reach
|
|
||||||
it". And the proxy answers from whichever vhost matches the Host header,
|
|
||||||
so a container with no public route was getting another site's version
|
|
||||||
back and failing deploys that had worked.
|
|
||||||
Reading it from a container on the shared network also exercises the
|
|
||||||
real HTTP path, so it proves the app is serving rather than that its
|
|
||||||
database knows a version. Purely additive: projects without those two
|
|
||||||
keys behave exactly as before.
|
|
||||||
|
|
||||||
|
Two problems it closes. A build stamp is something many sites deliberately
|
||||||
|
do not serve publicly, and a checker that reads it over the public URL stops
|
||||||
|
working the moment that endpoint is blocked — reporting "unknown", which is
|
||||||
|
indistinguishable from "could not reach it". And the proxy answers from
|
||||||
|
whichever vhost matches the Host header, so a container with no public route
|
||||||
|
was getting another site's version back and failing deploys that had
|
||||||
|
worked.
|
||||||
|
|
||||||
|
Reading it from a container on the shared network also exercises the real
|
||||||
|
HTTP path, so it proves the app is serving rather than that its database
|
||||||
|
knows a version. Purely additive: projects without those two keys behave
|
||||||
|
exactly as before.
|
||||||
|
|
||||||
|
v1.0.0.0.14 - 2026-08-20
|
||||||
|
------------------------
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
|
-----
|
||||||
- scp no longer receives an ssh-only flag — the stdin-hang fix added
|
- scp no longer receives an ssh-only flag — the stdin-hang fix added
|
||||||
-n to Get-Ec2SshOpts, and the deploy path splats that same array into
|
-n to Get-Ec2SshOpts, and the deploy path splats that same array into
|
||||||
scp as well as ssh. OpenSSH's scp has no -n: it exits 1 with
|
scp as well as ssh. OpenSSH's scp has no -n: it exits 1 with
|
||||||
@ -50,6 +247,8 @@ Fixed
|
|||||||
without checking; it now points at scp's own output, where the real
|
without checking; it now points at scp's own output, where the real
|
||||||
diagnosis already was.
|
diagnosis already was.
|
||||||
|
|
||||||
|
v1.0.0.0.8 - 2026-08-12
|
||||||
|
-----------------------
|
||||||
|
|
||||||
Fixed
|
Fixed
|
||||||
-----
|
-----
|
||||||
@ -63,16 +262,19 @@ Fixed
|
|||||||
there is no prompt on this path worth answering. ServerAlive* bounds a
|
there is no prompt on this path worth answering. ServerAlive* bounds a
|
||||||
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
session that dies mid-command (dropped VPN, sleeping laptop, rebooting
|
||||||
host) to about a minute instead of hanging.
|
host) to about a minute instead of hanging.
|
||||||
|
|
||||||
- Vendored archives survive the archive filter — files under a
|
- Vendored archives survive the archive filter — files under a
|
||||||
vendor/ directory are exempt from the "no archives in the zip" rule.
|
vendor/ directory are exempt from the "no archives in the zip" rule.
|
||||||
A project that vendors a dependency as vendor/*.tgz needs it in the
|
A project that vendors a dependency as vendor/*.tgz needs it in the
|
||||||
deploy zip; dropping it makes a Dockerfile's COPY vendor ./vendor
|
deploy zip; dropping it makes a Dockerfile's COPY vendor ./vendor
|
||||||
fail at image build, a confusing way to learn the filter ate a build
|
fail at image build, a confusing way to learn the filter ate a build
|
||||||
input.
|
input.
|
||||||
|
|
||||||
- unzip install is idempotent — the remote step ran
|
- unzip install is idempotent — the remote step ran
|
||||||
apt-get update && apt-get install -y unzip on every deploy; it now
|
apt-get update && apt-get install -y unzip on every deploy; it now
|
||||||
checks command -v unzip first and skips the apt round-trip when the
|
checks command -v unzip first and skips the apt round-trip when the
|
||||||
binary is already there.
|
binary is already there.
|
||||||
|
|
||||||
- zdeploy edge kind now ships asset subdirectories (#42) — the edge
|
- zdeploy edge kind now ships asset subdirectories (#42) — the edge
|
||||||
deploy uploaded top-level files only, so a project self-hosting assets
|
deploy uploaded top-level files only, so a project self-hosting assets
|
||||||
in folders (fonts/, vendor/) lost them on every deploy: docker
|
in folders (fonts/, vendor/) lost them on every deploy: docker
|
||||||
@ -81,15 +283,9 @@ Fixed
|
|||||||
loading. Every subdirectory except nginx-logs/ and .git/ now ships
|
loading. Every subdirectory except nginx-logs/ and .git/ now ships
|
||||||
recursively, and the ensure edge dir chown is recursive so scp into
|
recursively, and the ensure edge dir chown is recursive so scp into
|
||||||
docker-created root-owned dirs cannot fail.
|
docker-created root-owned dirs cannot fail.
|
||||||
- zdeploy no longer deletes operator-managed files on deploy (#2) —
|
|
||||||
the project-directory replacement preserved only ./.env, silently
|
v1.0.0.0.0 - 2026-07-28
|
||||||
destroying every other server-side file (.env.db, staged signing
|
-----------------------
|
||||||
keys, certs) on every deploy. All .env* files at the project root are
|
|
||||||
now preserved by default, plus anything listed in the new
|
|
||||||
deploy.preserve array (files or directories); the vite kind, which
|
|
||||||
previously preserved nothing, gets the same protection. Found the hard
|
|
||||||
way: a first production deploy of an auth service wiped its staged DB
|
|
||||||
credentials and RSA signing keys.
|
|
||||||
|
|
||||||
Added
|
Added
|
||||||
-----
|
-----
|
||||||
@ -104,6 +300,7 @@ Added
|
|||||||
hash verifies the download, the bundled CHECKSUMS.txt verifies the
|
hash verifies the download, the bundled CHECKSUMS.txt verifies the
|
||||||
extracted contents, so nobody needs to clone the repo to get a verifiable
|
extracted contents, so nobody needs to clone the repo to get a verifiable
|
||||||
copy. Released zips are immutable — zrelease refuses to overwrite one.
|
copy. Released zips are immutable — zrelease refuses to overwrite one.
|
||||||
|
|
||||||
- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
|
- zchecksums + CHECKSUMS.txt — a SHA-256 manifest covering every .ps1
|
||||||
and .cmd, so a download can be verified before anything is run. zchecksums
|
and .cmd, so a download can be verified before anything is run. zchecksums
|
||||||
checks them; zchecksums -Update regenerates after an intentional edit. The
|
checks them; zchecksums -Update regenerates after an intentional edit. The
|
||||||
@ -114,15 +311,18 @@ Added
|
|||||||
It's an integrity check, not a signature — the manifest sits in the same repo
|
It's an integrity check, not a signature — the manifest sits in the same repo
|
||||||
as the code, so it catches corruption and accidental drift, not a compromised
|
as the code, so it catches corruption and accidental drift, not a compromised
|
||||||
repo. A Pester test fails if the manifest ever goes stale.
|
repo. A Pester test fails if the manifest ever goes stale.
|
||||||
|
|
||||||
- Test suite (Pester) — the toolkit now has automated coverage of its own
|
- Test suite (Pester) — the toolkit now has automated coverage of its own
|
||||||
pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
|
pure logic: Get-ArchiveExcludes (including the deploy-vs-backup rule that
|
||||||
keeps .env/uploads out of deploys but in backups), config and project
|
keeps .env/uploads out of deploys but in backups), config and project
|
||||||
lookups, remote.composeDir fallback, EC2 target composition, and build-label
|
lookups, remote.composeDir fallback, EC2 target composition, and build-label
|
||||||
formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
|
formatting. Run with Invoke-Pester .\tests (Pester 5+). Verified by mutation
|
||||||
testing — reintroducing each historical bug turns the suite red.
|
testing — reintroducing each historical bug turns the suite red.
|
||||||
|
|
||||||
- ZCONFIG environment variable — overrides the path to zconfig.json, so
|
- ZCONFIG environment variable — overrides the path to zconfig.json, so
|
||||||
a run can target an alternate config. Also gives the test suite a seam for
|
a run can target an alternate config. Also gives the test suite a seam for
|
||||||
injecting a fixture.
|
injecting a fixture.
|
||||||
|
|
||||||
- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
|
- zec2_rotatekeys — safely rotate/reset server-side secrets — a new
|
||||||
tool for when a secret leaks or a deploy overwrites a production .env
|
tool for when a secret leaks or a deploy overwrites a production .env
|
||||||
with dev values. -Rotate KEY regenerates a key on the server
|
with dev values. -Rotate KEY regenerates a key on the server
|
||||||
@ -135,10 +335,12 @@ Added
|
|||||||
the container (up -d --force-recreate, so the new values actually load —
|
the container (up -d --force-recreate, so the new values actually load —
|
||||||
a plain restart keeps the old environment). -WhatIf previews the plan
|
a plain restart keeps the old environment). -WhatIf previews the plan
|
||||||
without touching anything.
|
without touching anything.
|
||||||
|
|
||||||
- zkill all — zkill now accepts all, stopping the dev server of
|
- zkill all — zkill now accepts all, stopping the dev server of
|
||||||
every project that has a ports.dev (edge/docker stacks with no local dev
|
every project that has a ports.dev (edge/docker stacks with no local dev
|
||||||
server are skipped). Brings it in line with zdeploy all / zbackup all;
|
server are skipped). Brings it in line with zdeploy all / zbackup all;
|
||||||
the one-shot "stop everything I've got running locally".
|
the one-shot "stop everything I've got running locally".
|
||||||
|
|
||||||
- zdeploy server-side health verification (verify block) — projects
|
- zdeploy server-side health verification (verify block) — projects
|
||||||
not published through the edge proxy can declare
|
not published through the edge proxy can declare
|
||||||
"verify": { "port": ..., "path": "/health", "expect": "..." } and the
|
"verify": { "port": ..., "path": "/health", "expect": "..." } and the
|
||||||
@ -146,16 +348,19 @@ Added
|
|||||||
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
over SSH) instead of hitting the public IP. Fixes a false PASS where the
|
||||||
proxy's default vhost answered for apps that never started; projects
|
proxy's default vhost answered for apps that never started; projects
|
||||||
with neither domain nor verify are now reported as NOT verified.
|
with neither domain nor verify are now reported as NOT verified.
|
||||||
|
|
||||||
- zdeploy optional deploy.gitPull — git pull --ff-only in the
|
- zdeploy optional deploy.gitPull — git pull --ff-only in the
|
||||||
project root before zipping. zdeploy zips the working tree and doesn't
|
project root before zipping. zdeploy zips the working tree and doesn't
|
||||||
otherwise pull, so a checkout left behind origin after a merged PR would
|
otherwise pull, so a checkout left behind origin after a merged PR would
|
||||||
deploy stale code while still bumping the build number — success that
|
deploy stale code while still bumping the build number — success that
|
||||||
changes nothing. A failed pull aborts the deploy instead.
|
changes nothing. A failed pull aborts the deploy instead.
|
||||||
|
|
||||||
- Per-project start config block — zstart honors optional pre-start
|
- Per-project start config block — zstart honors optional pre-start
|
||||||
steps from zconfig.json: "gitPull": true runs git pull --ff-only in
|
steps from zconfig.json: "gitPull": true runs git pull --ff-only in
|
||||||
the project root before starting (never boot a stale checkout), and
|
the project root before starting (never boot a stale checkout), and
|
||||||
"env": { ... } sets environment variables for the dev-server process.
|
"env": { ... } sets environment variables for the dev-server process.
|
||||||
Example added to zconfig.example.json.
|
Example added to zconfig.example.json.
|
||||||
|
|
||||||
- Switch-style argument tolerance — a leading dash on a project key is
|
- Switch-style argument tolerance — a leading dash on a project key is
|
||||||
ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
|
ignored everywhere (zdeploy -myapp == zdeploy myapp), for hands that
|
||||||
grew up on per-project switches.
|
grew up on per-project switches.
|
||||||
@ -167,19 +372,34 @@ Changed
|
|||||||
all does what bare invocation used to (matching zdeploy). The
|
all does what bare invocation used to (matching zdeploy). The
|
||||||
scheduled task created by setup_backup_schedule.ps1 passes all —
|
scheduled task created by setup_backup_schedule.ps1 passes all —
|
||||||
re-run it if your task was registered before this change.
|
re-run it if your task was registered before this change.
|
||||||
|
|
||||||
- zbackup parses more DATABASE_URL styles — double/single-quoted
|
- zbackup parses more DATABASE_URL styles — double/single-quoted
|
||||||
values (Prisma convention), postgres:// and postgresql+driver://
|
values (Prisma convention), postgres:// and postgresql+driver://
|
||||||
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
schemes, and URLs without an explicit port (defaults to 5432) all work;
|
||||||
previously these skipped the Postgres dump with "Could not parse
|
previously these skipped the Postgres dump with "Could not parse
|
||||||
DATABASE_URL".
|
DATABASE_URL".
|
||||||
|
|
||||||
- zkill / port cleanup kills the whole process tree — listeners on a
|
- zkill / port cleanup kills the whole process tree — listeners on a
|
||||||
project's port are now terminated children-first. Auto-reloading servers
|
project's port are now terminated children-first. Auto-reloading servers
|
||||||
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
(uvicorn/watchfiles, nodemon) spawn workers that inherit the listening
|
||||||
socket; killing only the parent left orphans serving stale code.
|
socket; killing only the parent left orphans serving stale code.
|
||||||
|
|
||||||
- zbackup finds DATABASE_URL in backend\.env too — projects with a
|
- zbackup finds DATABASE_URL in backend\.env too — projects with a
|
||||||
frontend/backend split get their Postgres dump bundled without needing a
|
frontend/backend split get their Postgres dump bundled without needing a
|
||||||
root-level .env.
|
root-level .env.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
-----
|
||||||
|
- zdeploy no longer deletes operator-managed files on deploy (#2) —
|
||||||
|
the project-directory replacement preserved only ./.env, silently
|
||||||
|
destroying every other server-side file (.env.db, staged signing
|
||||||
|
keys, certs) on every deploy. All .env* files at the project root are
|
||||||
|
now preserved by default, plus anything listed in the new
|
||||||
|
deploy.preserve array (files or directories); the vite kind, which
|
||||||
|
previously preserved nothing, gets the same protection. Found the hard
|
||||||
|
way: a first production deploy of an auth service wiped its staged DB
|
||||||
|
credentials and RSA signing keys.
|
||||||
|
|
||||||
1.0.0
|
1.0.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|||||||
@ -1,42 +1,45 @@
|
|||||||
686042d0ef32ed705c277f2341440762fdfe527eeb287381482ff681782e5aad setup_backup_schedule.ps1
|
6e95736007b3906950d95a830705ac1118ebfc11836d2c717d6dd49cb3157966 setup_backup_schedule.ps1
|
||||||
e35d91a175c29dcbe285b55397371a584000aab9ae3de6839f8b31def1d9dfd0 token-count.ps1
|
d6ab2ddb273a8ca870bd0673cffdd0907f16f4718367b4413a3e34bb1f1769c8 token-count.ps1
|
||||||
6d6372305aade2b51f4f0c31685b19de1de63feab7dd2f434aaed08926add010 zbackup.cmd
|
30ccf43c371ae95cecd5b443d9214ac8ea71ec83be94bc15bcf359beefb8dee1 zbackup.cmd
|
||||||
4b1542d1de2530df93daf20f1a10a8ab230ac4af754e8b05514527f0e49c9d32 zbackup.ps1
|
b3549b346c90a8ae5a05c4c91b7ac370f72467c4cb0d019edcd0ea80b994393e zbackup.ps1
|
||||||
1665236962a4b3936f98adbdb6c679fc761b809a64a28e8cad15311b6749d50e zbackup_and_sync.ps1
|
b6e5e15f8e2b128219c7b8b9db3e9b7804eee60fd6aed43f184210048e518f3c zbackup_and_sync.ps1
|
||||||
795fa2363c8ca4c67851b0513a9d8ac8e18a948d359f8168a62b7963bd02843d zbackup_ec2.cmd
|
b407ef5b298cd6fe94af492e9254b4447e34333d22f8c29d3e9d4d2881651cf3 zbackup_ec2.cmd
|
||||||
7c272c055d891ccdabde1fc8191142cbf5fb14054d5abeaec9906ca8c73fc84c zbackup_ec2.ps1
|
55df5f2e19e81317b1e33b7b030b252fc4d6f49c0c77229d9e4e86affeb3b8ef zbackup_ec2.ps1
|
||||||
49d48d55bab1b9ee5bc66cb96340a20fa50241b6a3558d4c518db3e06d4553e6 zchecksums.cmd
|
64bc3148b09e422ffe4890339fade2354a31f2c7ba004f2728c29a0a39c32f08 zchecksums.cmd
|
||||||
e0cbc6f263c129d89e87e8e7356fb256f59f21a6a4b209e191e7c70a3611286e zchecksums.ps1
|
3e1c573e446238cae494d22d25376f278333dc25fdbc0a5abbdaa9349722b438 zchecksums.ps1
|
||||||
bd5563bf74b83423aca49a56450f3712a9aec3d6a59f8b7862d5c5988c6defd2 zdeploy.cmd
|
049f8e79fc8c3d8d96ae8ccb1155d191e2dbf2753f4d4d73ceca00aa1ab0481b zdeploy.cmd
|
||||||
4a55ed714358b910fd2f326a6222e2ebaa99a1321df025a19edb6bbe0da13e76 zdeploy.ps1
|
d2ead96436507c8efedc3c72045e623137a1f844940c4ff57c9c7ebe67645511 zdeploy.ps1
|
||||||
1883d7307682c68bf3786203f54e9abfe12fcb09e3856190e3a7af5fffb2a16a zec2.cmd
|
30918a9260cb6220d6e2140edcf319fc0fa1e75b7fe36d0efe68b0d10cdde241 zec2.cmd
|
||||||
f9d0406f97e19303d8b368df8aaf70e5011a99703f67ca7b2b526e82a3c68789 zec2.ps1
|
695ec87ea6518933ee64524d369f1d0d5128bf6e54db31f555a888bdddcb9326 zec2.ps1
|
||||||
e1da88e4af6d5c88cc95231dc544ef29440d306e70739d0ba1f72795d359778d zec2_rotatekeys.cmd
|
ce3209ed8eaab242286e76aeb11b1249239e2804e3de456a1609cc7caa780b43 zec2_rotatekeys.cmd
|
||||||
ea2fcf081491dbb98794bc2ef6ce41563fef8a7c3553840e75aff757ea59d582 zec2_rotatekeys.ps1
|
f58277779b5c54814c29dda55ab567960e305fa9ef515b1206076be97f623093 zec2_rotatekeys.ps1
|
||||||
b30b10ca6d7930021f565a3d90eda967f1ca18366b30a671eab0668f6d5695eb zec2online.cmd
|
30ed73711eeddb518e02eb5e1968b7cc1dad51f27ecf42155b515b097487c3f3 zec2online.cmd
|
||||||
5b8c3bfc707255f5782ea6906075817f5bfe8d77fb1a4299a33581d0c70b9569 zec2online.ps1
|
1e8814928910ff4f3da59478511feb75280ba2e12111840fb47d2c5504741f0f zec2online.ps1
|
||||||
d550f9866c1f746da065021e5150566b3aebc07a51aa01ba6459f29560765b23 ZHelpers.ps1
|
4ea81dc9e3cafc514b1e8224e633bf22bd88bfdc6e53545ff88a2a2d33715b4f ZHelpers.ps1
|
||||||
1167972175b9d60613d1b8da55053c13a390037fd075fd8e411374d7a402ff72 zkill.cmd
|
d97f9b5c68526c3295796b3042ee86194721eacab56b0efa0f3fdf16c881b0ea zkill.cmd
|
||||||
0a4c09c2a85bfdd8577ee43179128fc6b14335e5232a2b172f4558542b8706f6 zkill.ps1
|
587de0b176788de917e713915ac468e44bf00cf9c9c7d6ea9b88aaec41ad6ea7 zkill.ps1
|
||||||
b81c06fc85045b69b298450b84757c1997ea14354e9774d579c27e41e2f43515 ZKiller.ps1
|
66556f55688d1f31e890d2b36143d3d0e2f5fcdded562e3d0a9591c02e827cda ZKiller.ps1
|
||||||
c785d57fd769056f08d8c434c3a215865b4f5d635cb37c64b16ecec8452f3843 ZKillOnly.ps1
|
2c9e0fa5dabb1544b6600cb60e5f66e89c787f217db246553b830152ab976ec3 ZKillOnly.ps1
|
||||||
f25aaa5818f88a6ce7c870ee43106a22dd98e8223fdf36dd6c8300934c7f3dd3 zrelease.cmd
|
479d01a4c962eb1dba2dbfee913c3704048f29581a0a06d068c0fb9145c3a51c zmerge.ps1
|
||||||
674f26a7525363e0ef3958d801843832e8299b83dc848e74c8e72b641b040e76 zrelease.ps1
|
dd1cfde33aec53fc0df4a34e45704a59bf48e094fcafcc0ec49e9d13aa442b6f zpull.cmd
|
||||||
55e0fa07ce5315bcd7909fcd2ba8b8efb5bfd47f7ad8a267259e43e972512b50 zrepair.cmd
|
9e746ad18b92ee7344061847b38c870f280421cf55d31e36a72a1514772e53a2 zpull.ps1
|
||||||
07e7d37b1d9cba52b4b1901b256038f3c7104f97a8881e850b29a17c59740009 zrepair.ps1
|
4a57e270fc75ae5419bd27cf01b7dd6d8965be8dd58e0c80af985cb7bf27bdc5 zrelease.cmd
|
||||||
ef1643e3063c5f4dfc13c5ec24a5efaeac0efb6c71ed13ae82c9a1f556826ca3 zrestart.cmd
|
a6906e118f13442340cc4e0b14d7523d63e85d2bdb0eff8fd3a7c25567af8962 zrelease.ps1
|
||||||
e5fea24fea50c64b08810989c271e1cccc0ef0d0e643aaa5165cda33e15955ba zrestart.ps1
|
58e5b604cc260af7e644412094ff6e3bf799f80f9e52b9a83a044299f7704107 zrepair.cmd
|
||||||
cf88610a2d3d3fa89ca247da506a9b284e6d182d857b39a2f8ff7079f15d402e zrestartd.cmd
|
186f7b0fb72808466ece328ea1a4e3a97bb2d4bae9f204ff7055e8b7f009614b zrepair.ps1
|
||||||
f691bd4da18cde9d1289881c87475ae10f1eb810e9f8a3e06cec7389f4d132ea zsetup.cmd
|
d97c9cf55b94c53dba49471d13f5c9870f2f50627ac5732ac4b60222da936244 zrestart.cmd
|
||||||
7ab2bdd2252d9c2a5603ff8cdd2c59bbea86b59103bf4fbd539710bc41293894 zsetup.ps1
|
32a8484d356c5f740d7a12f18bf0dd51a0c7da66010f6a6f553d976484315eef zrestart.ps1
|
||||||
0e643fc314d38e3fe3b568cf01e2d3882325672b957d161090b961df134e3f0a zsetup_mail.ps1
|
2ebf37f72323cac3963a4a95d1ae414fecf4da1e1e618147663c6cf94d1ece24 zrestartd.cmd
|
||||||
83fd1b810a2f06f23b4c537a298a4a4d8a93d3fdb796fcf7182bb301aa33f15a zstart.cmd
|
0a15ad0d341a5efdcd4c14e22e91d4b45f2bf87d0c5608fcf985200f6797bab4 zsetup.cmd
|
||||||
696e90d6fe55e65320f019e700f7ad62975635bf7c081892e37606cb10734a03 zstart.ps1
|
0d3042c5e44c3edf396005d0177f744c0c29536c25a50743e93a92156aa2a96c zsetup.ps1
|
||||||
9a3e86313347fce76e144009879bbe096b8ce6341da9e3fd3eea069b92cb492f zstart_docker.cmd
|
498ad7f8a7a56b332fede299e7172211ba360e76d9244c6c93f7cceae8a58619 zsetup_mail.ps1
|
||||||
9d9efe1cd048932dd6d7b7684bcbfebe7fe075a1852788ccbdde7c81485e0336 zstart_docker.ps1
|
46b3782d9b05649bdb1bcbd2a007bf6abc889abee420ac2f6e75530fa45a4694 zstart.cmd
|
||||||
22b9970f98bbbb59f8b8f790b440ef00dec0c579797bee1d41377112deb32665 zstartd.cmd
|
4b50cadf761f285f4fe655b143e04ec6b3565646ee8fb1ed165c234a201802b7 zstart.ps1
|
||||||
8013fb860d65024656edc8e583d1fa1737888f0461e66eb127d3c870fb235c14 zstop.ps1
|
2d74dcca3d4f51a28c84fbef5cc134126b61dfcc8ffb35e2d72f9596bf685809 zstart_docker.cmd
|
||||||
d63715a6323392f7514f5a93ce799e82a80f3556029a3cdc4665c92a0efec948 zsync.cmd
|
5016654d9ad68e11b85594be5e05318e19c5c6154174f734c43251970bc2086f zstart_docker.ps1
|
||||||
6fe6ea3c3ab33fb25a16aaae56b7fe8d1acda1871877e2110fdad7c5b8696d7a zsync.ps1
|
1e1c5990e1dba41a165e797790f73cd33fcee5035eec65dd0d2610ea8b56a023 zstartd.cmd
|
||||||
2ed882c5110cda66932618456127b2ed9e8a2b6ebe17480102b68224c4a9164d zversion.cmd
|
a894876ad9fa1bfac6cacfec2a837914debb773e05b975d9b3cdf56589dd8ed2 zstop.ps1
|
||||||
78f8f7df59288970354026f7467a838e179564be2e3b494658678835a2280422 zversion.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
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
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.
|
||||||
41
README.md
41
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
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
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": {
|
"deploy": {
|
||||||
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
"zipName": "MyAppDeploy.zip", // optional: defaults to <key>Deploy.zip
|
||||||
"gitPull": true, // optional: git pull --ff-only before zipping
|
"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
|
"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:
|
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>`.
|
- **`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.
|
- **`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.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.
|
- **`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`.
|
- **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`).
|
- **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) |
|
| `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) |
|
| `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` |
|
| `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
|
### Local development
|
||||||
|
|
||||||
@ -378,6 +382,21 @@ Give the project a `verify` block instead, and `zdeploy` checks the app **from t
|
|||||||
|
|
||||||
`port` is the host port the app publishes on the server; `path` defaults to `/`; `expect` is an optional substring the response must contain (an app version string makes this equivalent to build-number verification). Python-kind projects use `verify` automatically when there's no `build_version_tool.py` — and projects with *neither* a `domain` nor a `verify` block are now honestly reported as **NOT verified** instead of green-lighting the proxy's default page.
|
`port` is the host port the app publishes on the server; `path` defaults to `/`; `expect` is an optional substring the response must contain (an app version string makes this equivalent to build-number verification). Python-kind projects use `verify` automatically when there's no `build_version_tool.py` — and projects with *neither* a `domain` nor a `verify` block are now honestly reported as **NOT verified** instead of green-lighting the proxy's default page.
|
||||||
|
|
||||||
|
Two more keys make the check unambiguous and patient:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"verify": {
|
||||||
|
"port": 8005, "path": "/health",
|
||||||
|
"viaProxy": "my_edge_proxy", "upstream": "myapp_container:8000",
|
||||||
|
"timeoutSeconds": 120
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`viaProxy` + `upstream`** — read the version **over the docker network**, by exec-ing a curl inside the named container (usually the edge proxy, since it is on every stack's network). This is the most trustworthy channel there is: it cannot answer from the wrong product, and it works for apps that publish no host port at all.
|
||||||
|
- **`timeoutSeconds`** — how long verification may wait. An app that runs database migrations in its entrypoint exceeds a 30-second window *on every deploy that ships one*, and a warning that fires on routine success teaches you to ignore the one that matters.
|
||||||
|
|
||||||
|
Verification walks its channels in trust order — docker-network `viaProxy`, then `localhost:<port>`, then the edge with the project's own `Host` header — and **re-picks the channel on every retry**. That last part is the point: the check runs while the app is restarting, which is exactly when the good channels are briefly down, and locking the choice in up front is how a whole verification window gets spent asking the wrong thing. A project with no domain is **never** asked for via the edge: without a `Host` header the proxy can only answer from its default vhost — a different product — and no answer beats somebody else's answer.
|
||||||
|
|
||||||
`zec2` and `zec2online` use these same endpoints to show what's live and flag local/server version drift.
|
`zec2` and `zec2online` use these same endpoints to show what's live and flag local/server version drift.
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -392,6 +411,24 @@ Give the project a `verify` block instead, and `zdeploy` checks the app **from t
|
|||||||
|
|
||||||
Each release is packaged as a zip in [`releases/`](releases/) — grab the latest `zscripts-v*.zip`, check it, unzip, done:
|
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
|
```bash
|
||||||
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
|
sha256sum -c zscripts-v1.0.0.0.0.zip.sha256 # verify the download
|
||||||
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
unzip zscripts-v1.0.0.0.0.zip -d zscripts # extract
|
||||||
|
|||||||
454
README.txt
Normal file
454
README.txt
Normal file
@ -0,0 +1,454 @@
|
|||||||
|
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.
|
||||||
|
|
||||||
|
zscripts Token Savers
|
||||||
|
=====================
|
||||||
|
|
||||||
|
When an AI coding agent orchestrates your infrastructure — starting dev servers, deploying to EC2, diagnosing 502s — it spends hundreds to thousands of tokens per operation on SSH plumbing, Docker output, and retry logic. Those tokens should go to code.
|
||||||
|
|
||||||
|
Token Savers gives you short, one-word commands to run those parts yourself: zdeploy myapp, zrepair myapp, zstart myapp. You handle the deterministic infrastructure; your agent handles code. Running these scripts manually instead of asking your agent to orchestrate them keeps measured script output out of your agent's context window — ~26,500 tokens per active development day at a typical run cadence. Per-run figures are measured; the daily total applies typical run counts. See TOKEN_SAVINGS.md (TOKEN_SAVINGS.md) for the numbers and method.
|
||||||
|
|
||||||
|
Every command is a tiny PowerShell script driven by a single JSON config file. The project key you define in that config is the command argument — add myapp to the config and zstart myapp, zdeploy myapp, zbackup myapp all just work, no script edits needed.
|
||||||
|
|
||||||
|
Requirements: Windows, PowerShell 5.1+, OpenSSH client (ssh/scp, ships with Windows 10/11), and Docker + docker compose on the remote host for the deploy scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Install
|
||||||
|
-------
|
||||||
|
|
||||||
|
No installer. Clone the repo and add the folder to your PATH:
|
||||||
|
|
||||||
|
git clone https://github.com/evomedia-net/evo.zscripts.git C:\tools\zscripts
|
||||||
|
|
||||||
|
# Add to your user PATH (new terminals pick it up automatically)
|
||||||
|
[Environment]::SetEnvironmentVariable(
|
||||||
|
"Path",
|
||||||
|
[Environment]::GetEnvironmentVariable("Path", "User") + ";C:\tools\zscripts",
|
||||||
|
"User"
|
||||||
|
)
|
||||||
|
|
||||||
|
Open a new terminal and every command below works from any directory. The .cmd wrappers invoke PowerShell with -ExecutionPolicy Bypass, so no execution-policy changes are needed — zstart myapp just works from cmd, PowerShell, or a VS Code terminal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Configure
|
||||||
|
---------
|
||||||
|
|
||||||
|
All machine-specific values (server IP, SSH user and key, folder paths, project definitions) live in one file: zconfig.json. It is gitignored — your secrets never leave your machine.
|
||||||
|
|
||||||
|
cd C:\tools\zscripts
|
||||||
|
copy zconfig.example.json zconfig.json
|
||||||
|
notepad zconfig.json
|
||||||
|
|
||||||
|
The example config ships with sample projects named by their kind — pyapp, viteapp, nextapp, edge, analytics. Rename the keys to your own project names; the key is what you type as the command argument. Add as many projects as you like — no script edits ever needed.
|
||||||
|
|
||||||
|
Set the ZCONFIG environment variable to point at a config somewhere else — handy for a second machine profile, or for running against a scratch config without touching your real one.
|
||||||
|
|
||||||
|
Config reference
|
||||||
|
----------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"ec2": {
|
||||||
|
"ip": "203.0.113.10", // your server's public IP
|
||||||
|
"user": "youruser", // SSH user on the server
|
||||||
|
"pemKey": "C:\\Users\\You\\.ssh\\key.pem", // path to your SSH private key
|
||||||
|
"stackRoot": "/home/youruser/stack" // parent dir for all deployed projects
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"temp": "C:\\dev\\temp", // deploy zips staged here (auto-deleted)
|
||||||
|
"backupsLocal": "C:\\dev\\backups\\projects", // zbackup output
|
||||||
|
"backupsEc2": "C:\\dev\\backups\\ec2", // zbackup_ec2 output
|
||||||
|
"scriptsRoot": "C:\\tools\\zscripts", // this folder
|
||||||
|
"oneDriveBackups": "" // zsync destination ("" disables)
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"myapp": {
|
||||||
|
"label": "My App", // display name in output
|
||||||
|
"kind": "python", // python | vite | nextjs | edge | docker
|
||||||
|
"localRoot": "C:\\dev\\myapp", // project folder on this machine
|
||||||
|
"startModule": "myapp.main", // python kind: runs "python -m myapp.main"
|
||||||
|
// "startApp": "app.main:app", // ...or, for ASGI/FastAPI: uvicorn app.main:app --port <dev> --reload
|
||||||
|
"install": "-e .", // optional: pip args `zsetup` uses (auto-detects "-e ." / "-r requirements.txt")
|
||||||
|
"ports": { "dev": 8080, "prod": 3000 }, // local dev port / direct server port
|
||||||
|
"domain": "www.myapp.com", // public domain (health checks + verification)
|
||||||
|
"start": { // optional zstart pre-steps
|
||||||
|
"gitPull": true, // git pull --ff-only before starting
|
||||||
|
"env": { "MYAPP_DEBUG": "1" } // env vars for the dev server process
|
||||||
|
},
|
||||||
|
"db": { "user": "dbuser", "name": "dbname" }, // optional: enables db dump/wait steps
|
||||||
|
"migrations": "prisma", // optional: run prisma migrate on deploy
|
||||||
|
"remote": {
|
||||||
|
"path": "/home/youruser/stack/myapp", // deploy target on the server
|
||||||
|
"composeDir": "/home/youruser/stack/myapp/docker", // optional: if compose isn't at path root
|
||||||
|
"appService": "app", // optional: compose service name override
|
||||||
|
"containerName": "myapp" // optional (vite): container to read build-version from
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Commands
|
||||||
|
--------
|
||||||
|
|
||||||
|
The .cmd wrappers are the everyday interface. Every command takes one or more project keys from your config; several also accept all. A leading dash is tolerated (zdeploy -myapp works the same as zdeploy myapp) for anyone with switch-style muscle memory.
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| zsetup <key> ... | Create the project's Python venv + install deps (or npm install for node) |
|
||||||
|
| zstart <key> ... | Start local dev server(s) |
|
||||||
|
| zstartd <key> ... | Same, detached (new window, returns immediately) |
|
||||||
|
| zkill <key> ... \| all | Kill local dev server(s) by port |
|
||||||
|
| zrestart <key> ... | Kill + start in one step |
|
||||||
|
| zrestartd <key> ... | Kill + start detached |
|
||||||
|
| zdeploy <key> ... \| all | Zip → upload → rebuild → verify a project on the server |
|
||||||
|
| zec2 [<key> ...] | Quick reachability check (TCP + HTTP + live build version) |
|
||||||
|
| zec2online [<key> ...] | Deep health check; auto-starts downed stacks, streams diagnostics |
|
||||||
|
| zrepair <key> ... | Audit + repair compose/proxy state on the server |
|
||||||
|
| zec2_rotatekeys <key> | Rotate/reset secret keys in a project's server-side .env (values generated server-side; never printed) |
|
||||||
|
| zbackup <key> ... \| all | Zip local project sources (+ DB dump) to the backups folder |
|
||||||
|
| zbackup_ec2 [<key> ...] | Pull DB dumps + server-side data files down from the server |
|
||||||
|
| zsync [<key>] | Copy new backups offsite (or build + mirror a vite dist) |
|
||||||
|
| zstart_docker | Run a local docker compose stack from scriptsRoot\docker\ |
|
||||||
|
| 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
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
zstart — start dev servers
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
zstart <project> [<project> ...] [-Port N] [-BindHost <host>] [-Detached]
|
||||||
|
|
||||||
|
Starts each project's dev server using the handler for its kind: python runs python -m <startModule> — or, for an ASGI/FastAPI app, uvicorn <startApp> (e.g. app.main:app) with the dev port and --reload — preferring the project's .venv; vite runs npm run dev -- --host --port, nextjs runs npm run dev with PORT set. Runs npm install automatically if node_modules is missing. A project's optional start config block runs first — gitPull fast-forwards the checkout and env sets process environment variables. Two more opt-in conveniences: if the project has a motd/ folder of .txt files, one is shown (rotating) at startup; if it has scripts/build_version_tool.py, the build number is bumped on each start.
|
||||||
|
|
||||||
|
zstart viteapp # dev server on its configured port
|
||||||
|
zstart pyapp -Port 9000 # override the port
|
||||||
|
zstart viteapp -BindHost 0.0.0.0 # expose on the LAN
|
||||||
|
zstartd nextapp # detached: window opens, prompt returns
|
||||||
|
|
||||||
|
zkill — stop dev servers
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
zkill <project> [<project> ...] | all [-Port N] [-KillAll]
|
||||||
|
|
||||||
|
Finds whatever is LISTENING on each project's dev port and kills it — along with its whole process tree, children first. That matters for auto-reloading servers (uvicorn/watchfiles, nodemon): their worker processes inherit the listening socket and would otherwise survive as orphans, serving stale code. -KillAll also hunts down stray node/python/next-server processes whose command line references the project folder. all targets every project that has a dev port — the one-shot "stop everything I've got running locally".
|
||||||
|
|
||||||
|
zkill viteapp # free the port
|
||||||
|
zkill pyapp viteapp nextapp # nuke everything
|
||||||
|
zkill all # stop every project's dev server
|
||||||
|
zkill nextapp -KillAll # also kill orphaned runtime processes
|
||||||
|
|
||||||
|
zrestart — kill then start
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
zrestart <project> [<project> ...] [-Port N] [-KillAll] [-NoRestart] [-Detached] [-BindHost <host>]
|
||||||
|
|
||||||
|
The "it's wedged, bounce it" command: kill phase, then start phase with the same flags. -NoRestart makes it kill-only; zrestartd restarts detached.
|
||||||
|
|
||||||
|
Server deployment & operations
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
zdeploy — deploy to the server
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
zdeploy <project> [<project> ...] [-Note "message"]
|
||||||
|
zdeploy all [-Note "message"]
|
||||||
|
|
||||||
|
The core workflow, per project kind (projects with deploy.gitPull first git pull --ff-only so a merged PR isn't left behind):
|
||||||
|
|
||||||
|
- python / vite / nextjs — zip the local source (excluding .git, node_modules, envs, archives, junk, plus anything in deploy.exclude), free disk space on the server (docker prune; aborts if under 1.5 GB free), scp the zip up, unzip into remote.path preserving all server-side .env* files plus anything listed in deploy.preserve (staged keys, certs, seed data — files or directories), docker compose build + up -d, then verify the live site reports the new build version (see Enabling deploy verification (#enabling-deploy-verification)). nextjs additionally waits for Postgres (db block) and applies migrations (migrations field). Zips are always deleted locally afterward.
|
||||||
|
- edge — uploads every top-level file in the edge folder (nginx.conf, compose, css, htpasswd, …), validates the new config with nginx -t before switching over, then recreates the proxy.
|
||||||
|
- docker — uploads the compose folder's files, docker compose pull + up -d. For stacks that run stock images (analytics, mail, etc.).
|
||||||
|
|
||||||
|
all deploys every project — edge kinds first, then the rest in config order — and stops at the first failure.
|
||||||
|
|
||||||
|
zdeploy viteapp
|
||||||
|
zdeploy pyapp -Note "fix billing banner"
|
||||||
|
zdeploy all -Note "weekly release"
|
||||||
|
|
||||||
|
zec2 — reachability check
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
zec2 [<project> ...] # no args = every project with a domain
|
||||||
|
|
||||||
|
For each project: TCP connect, then an HTTP GET with the project's domain as the Host header, then the live build version. Fast "is it up?" answer with firewall hints when it isn't.
|
||||||
|
|
||||||
|
zec2online — health check with auto-recovery
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
zec2online [<project> ...] # no args = every project with a domain
|
||||||
|
|
||||||
|
The heavier sibling: verifies each app over HTTP, compares the local build version against what the server is actually serving (a mismatch means "redeploy?" — or a stale cache), and if a site is down it SSHes in, runs docker compose up -d for the app and the edge proxy, waits up to 30 s, and streams compose logs and system diagnostics if recovery fails.
|
||||||
|
|
||||||
|
zrepair — fix server routing
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
zrepair <project> [<project> ...]
|
||||||
|
|
||||||
|
Validates the edge proxy's nginx config (if an edge project is defined), shows each stack's compose status, starts anything that's down, and smoke-tests the live domain. For the "deploy succeeded but the site 502s" class of problem.
|
||||||
|
|
||||||
|
zstop.ps1 — stop server stacks
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
zstop <project> [<project> ...]
|
||||||
|
|
||||||
|
docker compose down for the selected stacks on the server. Data volumes are preserved; zdeploy <project> brings a stack back. (PowerShell script only, no .cmd wrapper.)
|
||||||
|
|
||||||
|
zec2_rotatekeys — rotate server-side secrets
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
zec2_rotatekeys <project> [-Rotate KEY,KEY] [-Set KEY,KEY] [-EnvFile rel/path] [-Restart] [-WhatIf]
|
||||||
|
|
||||||
|
For when a secret leaks or a deploy overwrites a production .env with dev values: rotate or reset keys in a project's server-side .env without the values ever passing through this machine's shell history, a command argument, or your screen. -Rotate keys are regenerated on the server with openssl rand -hex 32 — the new value is written straight into the .env there and never leaves the box. -Set keys are typed into a masked prompt and streamed to the server over SSH stdin (never a command argument, never echoed), for operator-known values like DATABASE_URL or ADMIN_EMAIL. The current server .env is copied to a timestamped .bak before any change; the KEY line is updated atomically, matching an existing key or appending it. The env file is auto-detected from the project's deploy.preserve (first *.env) or defaults to .env — override with -EnvFile backend/.env. Nothing touches the running app unless you pass -Restart, which recreates the container (docker compose up -d --force-recreate <svc>) so it actually reloads the new .env — a plain restart would keep the old environment. Being high-impact, it confirms before writing; -WhatIf prints the exact plan and changes nothing.
|
||||||
|
|
||||||
|
# Preview only — see exactly what would change, change nothing:
|
||||||
|
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL -WhatIf
|
||||||
|
|
||||||
|
# Regenerate the JWT secret, restore the operator-known values, then restart:
|
||||||
|
zec2_rotatekeys pyapp -Rotate JWT_SECRET -Set DATABASE_URL,ADMIN_EMAIL,ADMIN_PASSWORD -Restart
|
||||||
|
|
||||||
|
Backups
|
||||||
|
-------
|
||||||
|
|
||||||
|
zbackup — local backups
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
zbackup <project> [<project> ...] [-Tag "label"]
|
||||||
|
zbackup all # every project + this scripts folder
|
||||||
|
zbackup scripts # just this scripts folder ('scripts' is reserved)
|
||||||
|
|
||||||
|
Zips each project's source into paths.backupsLocal\<key>\<timestamp>_<key>[_tag].zip. If the project's .env declares a DATABASE_URL, a Postgres dump is bundled into the zip automatically — quoted values (Prisma-style), postgres:///postgresql+driver:// schemes, and URLs without an explicit port all parse. backend\.env is checked too, for frontend/backend split projects. -Tag labels the archive — handy before risky changes.
|
||||||
|
|
||||||
|
zbackup all # everything
|
||||||
|
zbackup pyapp -Tag "pre-migration"
|
||||||
|
|
||||||
|
zbackup_ec2 — pull backups from the server
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
zbackup_ec2 [<project> ...] # no args = every project with a remote.path
|
||||||
|
|
||||||
|
For projects with a db block, runs pg_dump inside the server's db container. Also zips server-side data dirs (uploads/, archive/, dist/) when present, then downloads everything to paths.backupsEc2 and cleans up the remote temp files.
|
||||||
|
|
||||||
|
zsync — sync backups offsite
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
zsync # new backup files -> paths.oneDriveBackups
|
||||||
|
zsync <viteproject> -Destination <path> # npm run build, then mirror dist/ to path
|
||||||
|
|
||||||
|
The no-args mode copies only files that don't already exist at the destination (never overwrites, never deletes). The project mode is for mirroring a static build; it also honors $env:ZSYNC_DEST.
|
||||||
|
|
||||||
|
zbackup_and_sync.ps1 — both in one
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
zbackup_and_sync.ps1 <project> [<project> ...] | all
|
||||||
|
|
||||||
|
Runs zbackup, then zsync. This is what the scheduled task calls (with all).
|
||||||
|
|
||||||
|
setup_backup_schedule.ps1 — nightly automation
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
Run as Administrator once. Creates a Windows Scheduled Task that runs zbackup_and_sync.ps1 all daily at 2:00 AM.
|
||||||
|
|
||||||
|
Utilities
|
||||||
|
---------
|
||||||
|
|
||||||
|
zstart_docker — local compose stack
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
zstart_docker [-Build] [-Attached] [-Solo]
|
||||||
|
|
||||||
|
Brings up a docker compose stack from scriptsRoot\docker\docker-compose.yml (or docker-compose.solo.yml with -Solo). Checks that Docker Desktop is actually running and tells you how to unwedge it if not.
|
||||||
|
|
||||||
|
zsetup_mail.ps1 — provision mail accounts
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
zsetup_mail.ps1 -domain yourdomain.com [-mailHost mail.yourdomain.com]
|
||||||
|
|
||||||
|
Creates admin@ and noreply@ mailboxes (with generated passwords) in a docker-mailserver container on the server, then prints the exact DNS records (MX, SPF, A) and SMTP/IMAP settings to plug into your app.
|
||||||
|
|
||||||
|
ZHelpers.ps1 — shared library
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Not run directly. Dot-sourced by the other scripts; provides config loading (Get-ZConfig, Get-ZProject), SSH helpers (Invoke-Ec2Step), the deploy/backup archiver (New-ProjectArchive), and process-kill helpers. Extend here if you're adding your own scripts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Enabling deploy verification
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Why not just check for HTTP 200? Because a 200 proves nothing — a stale cached build serves 200 all day. These scripts verify a deploy by comparing build numbers: your app exposes its build version, the deploy expects to see the new number live, and a mismatch means the upload or Docker build failed (or you're looking at a cached build).
|
||||||
|
|
||||||
|
It's optional — deploys still work without it, ending in a WARNING instead of a PASS — but it's the difference between "the server answered" and "the code I just shipped is actually running."
|
||||||
|
|
||||||
|
1. Add a version file to your project
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
// build-version.json (vite: in public/ · nextjs: in public/ · python: project root)
|
||||||
|
{ "productVersion": "1.0", "buildNumber": 42 }
|
||||||
|
|
||||||
|
2. Bump it during the server-side Docker build
|
||||||
|
----------------------------------------------
|
||||||
|
|
||||||
|
The convention: each deploy's Docker build increments buildNumber by one, so the deploy script expects local buildNumber + 1 to show up live. One line in your Dockerfile does it:
|
||||||
|
|
||||||
|
RUN node -e "const f='public/build-version.json',v=require('./'+f);v.buildNumber++;require('fs').writeFileSync(f,JSON.stringify(v))"
|
||||||
|
|
||||||
|
3. Expose it
|
||||||
|
------------
|
||||||
|
|
||||||
|
Vite / static sites — nothing to do: public/build-version.json is served at /build-version.json, which is where verification looks. (If your edge proxy blocks it from outside, set remote.containerName in config and verification reads it inside the container instead.)
|
||||||
|
|
||||||
|
Next.js — add an API route at /api/build-version:
|
||||||
|
|
||||||
|
// app/api/build-version/route.ts
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import bv from "@/public/build-version.json";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({ build_version: `v${bv.productVersion}.${bv.buildNumber}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
Python (FastAPI shown; any framework works) — expose /api/build-version:
|
||||||
|
|
||||||
|
import json, pathlib
|
||||||
|
|
||||||
|
@app.get("/api/build-version")
|
||||||
|
def build_version():
|
||||||
|
bv = json.loads(pathlib.Path("build-version.json").read_text())
|
||||||
|
return {"build_version": f"v{bv['productVersion']}.{bv['buildNumber']}"}
|
||||||
|
|
||||||
|
Python projects can go further with a scripts/build_version_tool.py supporting get / set / bump subcommands — if present, zdeploy bumps the version inside the running container, records it, and zstart bumps on every dev start.
|
||||||
|
|
||||||
|
Alternative: server-side health check (verify block)
|
||||||
|
----------------------------------------------------
|
||||||
|
|
||||||
|
Not every stack is published through the edge proxy — internal APIs, apps whose host port the firewall blocks, services waiting on a DNS record. For those, the old fallback (GET http://<server-ip>/) was worse than nothing: the edge proxy's default vhost answers with a 200 and the deploy "passes" even if your app never started.
|
||||||
|
|
||||||
|
Give the project a verify block instead, and zdeploy checks the app from the server itself over SSH:
|
||||||
|
|
||||||
|
"verify": { "port": 8005, "path": "/health", "expect": "\"status\":\"ok\"" }
|
||||||
|
|
||||||
|
port is the host port the app publishes on the server; path defaults to /; expect is an optional substring the response must contain (an app version string makes this equivalent to build-number verification). Python-kind projects use verify automatically when there's no build_version_tool.py — and projects with neither a domain nor a verify block are now honestly reported as NOT verified instead of green-lighting the proxy's default page.
|
||||||
|
|
||||||
|
Two more keys make the check unambiguous and patient:
|
||||||
|
|
||||||
|
"verify": {
|
||||||
|
"port": 8005, "path": "/health",
|
||||||
|
"viaProxy": "my_edge_proxy", "upstream": "myapp_container:8000",
|
||||||
|
"timeoutSeconds": 120
|
||||||
|
}
|
||||||
|
|
||||||
|
- viaProxy + upstream — read the version over the docker network, by exec-ing a curl inside the named container (usually the edge proxy, since it is on every stack's network). This is the most trustworthy channel there is: it cannot answer from the wrong product, and it works for apps that publish no host port at all.
|
||||||
|
- timeoutSeconds — how long verification may wait. An app that runs database migrations in its entrypoint exceeds a 30-second window on every deploy that ships one, and a warning that fires on routine success teaches you to ignore the one that matters.
|
||||||
|
|
||||||
|
Verification walks its channels in trust order — docker-network viaProxy, then localhost:<port>, then the edge with the project's own Host header — and re-picks the channel on every retry. That last part is the point: the check runs while the app is restarting, which is exactly when the good channels are briefly down, and locking the choice in up front is how a whole verification window gets spent asking the wrong thing. A project with no domain is never asked for via the edge: without a Host header the proxy can only answer from its default vhost — a different product — and no answer beats somebody else's answer.
|
||||||
|
|
||||||
|
zec2 and zec2online use these same endpoints to show what's live and flag local/server version drift.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Adding a new project
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
1. Add a key under projects in zconfig.json — copy the sample of the matching kind and rename it.
|
||||||
|
2. That's it: zstart, zkill, zrestart, zbackup, zdeploy, zec2, zec2online, zrepair, zstop all accept the new key immediately.
|
||||||
|
3. A project whose deploy doesn't fit the python/vite/nextjs/edge/docker patterns needs its own Invoke-<Kind>Deploy function in zdeploy.ps1 — copy an existing handler; they're all variations on zip → upload → compose up → verify.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
The zip contains every command, CHECKSUMS.txt, zconfig.example.json, and the docs. Versions follow v{major}.{rc}.{beta}.{alpha}.{build}; every script header carries the release version it shipped in, so even a single copied file can be traced to its release.
|
||||||
|
|
||||||
|
Verifying what you downloaded
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
CHECKSUMS.txt holds a SHA-256 for every .ps1 and .cmd in the repo. Check them before running anything:
|
||||||
|
|
||||||
|
zchecksums
|
||||||
|
|
||||||
|
Or with the standard tool on Linux/macOS/WSL — the manifest is sha256sum format:
|
||||||
|
|
||||||
|
sha256sum -c CHECKSUMS.txt
|
||||||
|
|
||||||
|
The hashes are identical on every platform: .gitattributes pins .ps1/.cmd to CRLF everywhere, so a file is byte-for-byte the same whether you cloned on Windows or Linux.
|
||||||
|
|
||||||
|
zchecksums flags three things — a file whose contents changed, a listed file that's gone, and a script on disk that isn't in the manifest (so something added quietly still gets noticed). It exits non-zero on any of them.
|
||||||
|
|
||||||
|
If you edit a script yourself, regenerate and commit the manifest with it:
|
||||||
|
|
||||||
|
zchecksums -Update
|
||||||
|
|
||||||
|
What this does and doesn't prove. CHECKSUMS.txt lives in the same repo as the scripts, so anyone who could alter a script could alter the manifest too. It's an integrity check, not a signature: it reliably catches a truncated clone, a local edit you forgot about, or a file added outside a commit. It does not prove the code came from this project — for that you'd need a signature or a hash published outside this repo.
|
||||||
|
|
||||||
|
Tests
|
||||||
|
-----
|
||||||
|
|
||||||
|
The toolkit has its own Pester (https://pester.dev) suite covering the pure logic — the exclude lists, config lookups, and version-label formatting that the deploy and backup paths depend on:
|
||||||
|
|
||||||
|
Invoke-Pester .\tests
|
||||||
|
|
||||||
|
Needs Pester 5+ (Install-Module Pester -Scope CurrentUser); Windows ships 3.x, which won't run these. The suite injects a fixture config through ZCONFIG, so it never reads your real zconfig.json and runs fine on a machine that has never been configured.
|
||||||
|
|
||||||
|
The high-value case is the deploy-vs-backup split: deploys must exclude .env files and uploads/, backups must keep them. Get that backwards in either direction and you either ship secrets to production or quietly write backups that can't restore — neither fails loudly at runtime.
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
- "zconfig.json not found" — you haven't copied zconfig.example.json yet. Every script tells you this and exits.
|
||||||
|
- "Unknown project key" — the argument doesn't match a key in zconfig.json; the error lists the valid keys.
|
||||||
|
- "PEM key not found" — fix ec2.pemKey in zconfig.json.
|
||||||
|
- Deploy aborts with "less than 1.5 GB free" — the server's disk is full even after auto-pruning. Grow the volume, or SSH in and run sudo docker system prune -af.
|
||||||
|
- Deploy ends with a version WARNING — the new build isn't what's being served: check the Docker build output, and see Enabling deploy verification (#enabling-deploy-verification) if you haven't set it up.
|
||||||
|
- Port already in use when starting — zkill <project> first, or just use zrestart.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
MIT (LICENSE)
|
||||||
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
|
Created by Kelly Michels · dev@evomedia.net
|
||||||
Licensed under the MIT License. See LICENSE.
|
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.
|
||||||
418
ZHelpers.ps1
418
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly.
|
# ZHelpers.ps1 — shared library dot-sourced by every z script. Not run directly.
|
||||||
|
|
||||||
@ -15,8 +15,8 @@ $script:ArchiveExtensions = @(
|
|||||||
# exempt from ArchiveExtensions. A project that vendors a dependency as
|
# exempt from ArchiveExtensions. A project that vendors a dependency as
|
||||||
# vendor/*.tgz (common when a bundler cannot resolve `file:` links outside the
|
# vendor/*.tgz (common when a bundler cannot resolve `file:` links outside the
|
||||||
# project root) needs that tarball in the deploy zip - dropping it makes a
|
# project root) needs that tarball in the deploy zip - dropping it makes a
|
||||||
# Dockerfile's `COPY vendor ./vendor` fail at image build, which is a confusing
|
# Dockerfile's `COPY vendor ./vendor` fail at image build, which is a
|
||||||
# way to discover the archive filter ate a required build input.
|
# confusing way to discover the archive filter ate a required build input.
|
||||||
$script:ArchiveKeepDirNames = @('vendor')
|
$script:ArchiveKeepDirNames = @('vendor')
|
||||||
$script:ScriptExtensions = @('.ps1', '.cmd', '.bat')
|
$script:ScriptExtensions = @('.ps1', '.cmd', '.bat')
|
||||||
$script:JunkExtensions = @(
|
$script:JunkExtensions = @(
|
||||||
@ -42,8 +42,9 @@ $script:JunkDirNames = @(
|
|||||||
$script:ZConfigCache = $null
|
$script:ZConfigCache = $null
|
||||||
|
|
||||||
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
# Where zconfig.json lives. Defaults to next to the scripts; override with the
|
||||||
# ZCONFIG environment variable. Useful for pointing a run at an alternate
|
# ZCONFIG environment variable, matching the bash port (zhelpers.sh does the
|
||||||
# config, and it is the seam the test suite uses to inject a fixture.
|
# same). Useful for pointing a run at an alternate config, and it is the seam
|
||||||
|
# the test suite uses to inject a fixture.
|
||||||
function Get-ZConfigPath {
|
function Get-ZConfigPath {
|
||||||
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
if ($env:ZCONFIG) { return $env:ZCONFIG }
|
||||||
return (Join-Path $PSScriptRoot "zconfig.json")
|
return (Join-Path $PSScriptRoot "zconfig.json")
|
||||||
@ -104,6 +105,33 @@ function Get-ZEdgeProject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Remote compose directory for a project: remote.composeDir if set, else remote.path.
|
# 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 {
|
function Get-RemoteComposeDir {
|
||||||
param([Parameter(Mandatory)][string]$Key)
|
param([Parameter(Mandatory)][string]$Key)
|
||||||
$proj = Get-ZProject -Key $Key
|
$proj = Get-ZProject -Key $Key
|
||||||
@ -122,18 +150,19 @@ function Get-Ec2Home {
|
|||||||
return "/home/$((Get-ZConfig).ec2.user)"
|
return "/home/$((Get-ZConfig).ec2.user)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Options every deploy-path ssh/scp carries. Splat with @sshOpts.
|
# Options every deploy-path ssh/scp carries. Splat with @sshOpts, matching the
|
||||||
|
# idiom in zsetup_mail.ps1 and zec2_rotatekeys.ps1.
|
||||||
#
|
#
|
||||||
# BatchMode=yes is the one that matters. Without it ssh PROMPTS - for a
|
# BatchMode=yes is the one that matters. Without it ssh PROMPTS - for a
|
||||||
# passphrase, a password, a sudo password - and waits forever. The deploy pipes
|
# passphrase, a password, a sudo password - and waits forever. The deploy pipes
|
||||||
# stderr into the pipeline (2>&1 | ForEach-Object) so the prompt is swallowed
|
# stderr into the pipeline (2>&1 | ForEach-Object) so the prompt is swallowed
|
||||||
# on its way to the screen: the run simply stops under whatever step label was
|
# on its way to the screen: the run simply stops under whatever step label was
|
||||||
# printed last, with nothing to explain it and no obvious reason why that
|
# printed last, with nothing to explain it and no obvious reason why that
|
||||||
# particular step would be slow. One deploy appeared to hang on "ensure shared
|
# particular step would be slow. `zdeploy umami` appeared to hang on "ensure
|
||||||
# web network", a step whose entire body is `docker network create web
|
# shared web network", a step whose entire body is `docker network create web
|
||||||
# 2>/dev/null || true` against a network that already existed.
|
# 2>/dev/null || true` against a network that already existed.
|
||||||
#
|
#
|
||||||
# There is no prompt here you would ever want to answer - a deploy key is
|
# There is no prompt here we would ever want to answer - the deploy key is
|
||||||
# unencrypted and sudo on the box is passwordless - so failing immediately is
|
# unencrypted and sudo on the box is passwordless - so failing immediately is
|
||||||
# strictly better than waiting on input that is never coming.
|
# strictly better than waiting on input that is never coming.
|
||||||
#
|
#
|
||||||
@ -147,16 +176,16 @@ function Get-Ec2Home {
|
|||||||
# reads its stdin and forwards it to the remote command, and under PowerShell it
|
# reads its stdin and forwards it to the remote command, and under PowerShell it
|
||||||
# inherits the console handle - so it can block forever waiting on input nobody
|
# inherits the console handle - so it can block forever waiting on input nobody
|
||||||
# is going to type. The timeouts above cannot help: they bound a connection that
|
# is going to type. The timeouts above cannot help: they bound a connection that
|
||||||
# is dying, and this one was never established. One deploy stopped under
|
# is dying, and this one was never established. A deploy on 2026-08-19 stopped
|
||||||
# "ensure unzip installed", a step whose body short-circuits when unzip is
|
# under "ensure unzip installed", a step whose body short-circuits on an already
|
||||||
# already present; the server showed no ssh session at all (`who` empty, no
|
# installed unzip; the server showed no ssh session at all (`who` empty, no
|
||||||
# docker build running), which is what a client-side stdin block looks like
|
# docker build running), which is what a client-side stdin block looks like from
|
||||||
# from the other end. The zip had uploaded and prod stayed a release behind.
|
# the other end. The zip had uploaded and prod stayed a PR behind.
|
||||||
#
|
#
|
||||||
# Safe here because nothing that pipes stdin INTO ssh uses these options:
|
# Safe here because nothing that pipes stdin INTO ssh uses these options:
|
||||||
# zdeploy passes only command strings. Any script that DOES pipe into ssh must
|
# zdeploy passes only command strings, and the scripts that do pipe
|
||||||
# build its own option array - adding -n to those would break them, so do not
|
# (one-off registration and key-rotation scripts) build their own
|
||||||
# hoist this beyond the deploy path.
|
# option arrays. Adding -n to those would break them - do not hoist it.
|
||||||
function Get-Ec2SshOpts {
|
function Get-Ec2SshOpts {
|
||||||
return @(
|
return @(
|
||||||
'-n',
|
'-n',
|
||||||
@ -170,9 +199,9 @@ function Get-Ec2SshOpts {
|
|||||||
|
|
||||||
# The same options for scp, which does NOT accept -n: OpenSSH's scp exits 1 with
|
# The same options for scp, which does NOT accept -n: OpenSSH's scp exits 1 with
|
||||||
# "unknown option -- n" and prints its usage block. That failure is easy to
|
# "unknown option -- n" and prints its usage block. That failure is easy to
|
||||||
# misread, because the caller's own error text is what the operator sees while
|
# misread, because the caller's own error text is what the operator sees and the
|
||||||
# the usage text scrolls past above it - one deploy reported "Likely server disk
|
# usage text scrolls past above it - a deploy on 2026-08-19 reported "Likely
|
||||||
# space" on a box with plenty of room.
|
# server disk space" while the box sat at 79% with 8.1 GB free.
|
||||||
#
|
#
|
||||||
# Derived from Get-Ec2SshOpts rather than duplicated, so the timeouts can never
|
# Derived from Get-Ec2SshOpts rather than duplicated, so the timeouts can never
|
||||||
# drift apart between the two transports.
|
# drift apart between the two transports.
|
||||||
@ -217,11 +246,11 @@ function Invoke-Ec2Step {
|
|||||||
#
|
#
|
||||||
# Deploys ship the default branch, so this SWITCHES to it rather than pulling
|
# Deploys ship the default branch, so this SWITCHES to it rather than pulling
|
||||||
# whatever branch happens to be checked out. The old behaviour pulled the
|
# whatever branch happens to be checked out. The old behaviour pulled the
|
||||||
# current branch, which breaks as soon as the remote deletes branches on merge:
|
# current branch, which broke the day delete_branch_on_merge went on
|
||||||
# a checkout still sitting on its just-merged PR branch pulls a ref the merge
|
# fleet-wide (2026-08-07): a repo still sitting on its just-merged PR branch
|
||||||
# deleted, and the deploy dies on "no such ref was fetched". Worse, when the ref
|
# pulls a ref the merge deleted, and the deploy dies on "no such ref was
|
||||||
# DID still exist, pulling the feature branch meant a deploy could ship a
|
# fetched". Worse, when the ref DID still exist, pulling the feature branch
|
||||||
# branch rather than the default.
|
# meant a deploy could ship a branch, not main.
|
||||||
#
|
#
|
||||||
# The switch refuses to run over local changes: a dirty tree aborts the deploy
|
# The switch refuses to run over local changes: a dirty tree aborts the deploy
|
||||||
# with the file list rather than risk tangling uncommitted work. The stale
|
# with the file list rather than risk tangling uncommitted work. The stale
|
||||||
@ -277,10 +306,9 @@ function Invoke-DeployGitPull {
|
|||||||
# unreviewed SOURCE shipping; a stamp the script just wrote is not
|
# unreviewed SOURCE shipping; a stamp the script just wrote is not
|
||||||
# that. It is still committed separately, one bump per release,
|
# that. It is still committed separately, one bump per release,
|
||||||
# per the versioning rule - this only stops it being a gate.
|
# per the versioning rule - this only stops it being a gate.
|
||||||
$deployWritten = @('build-version.json', 'CHANGELOG.md')
|
|
||||||
$dirty = $dirty | Where-Object {
|
$dirty = $dirty | Where-Object {
|
||||||
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
$path = ($_ -replace '^..\s+', '') -replace '^.*/', ''
|
||||||
$deployWritten -notcontains $path
|
(Get-DeployStampFiles) -notcontains $path
|
||||||
}
|
}
|
||||||
if ($dirty) {
|
if ($dirty) {
|
||||||
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
||||||
@ -313,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 ────────────────────────────────────────────────────
|
# ── Build-version helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
function Read-JsonBuildVersion {
|
function Read-JsonBuildVersion {
|
||||||
@ -328,32 +529,36 @@ function Get-ServerSideVersionCommand {
|
|||||||
$null when the project has not configured one.
|
$null when the project has not configured one.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Reading a live build number through the public proxy only works while
|
Four tools read a live build number, and all four did it by asking
|
||||||
that endpoint IS public - and a build stamp is something many sites
|
the public edge: zdeploy's post-deploy check, zec2, zec2online, and
|
||||||
deliberately do not serve to the world. Blocking it at the proxy
|
bash/zhelpers.sh. That works only for as long as the endpoint is
|
||||||
without moving the readers first leaves every tool quietly reporting
|
public, and it should not be: www's /build-version.json has been
|
||||||
"unknown", which looks identical to "could not reach it".
|
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 proxy is also how a check reads the WRONG service:
|
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
|
The proxy answers from whichever vhost matches the Host header, so a
|
||||||
container with no public route gets another site's version back.
|
service with no public route gets somebody else's version back --
|
||||||
|
one project's deploy check compared another project's build against
|
||||||
|
its own and reported a failure on a deploy that had worked.
|
||||||
|
|
||||||
A service reached only on a shared docker network cannot be curled
|
A project reached only on the shared docker network cannot be curled
|
||||||
from the host when it publishes no port. It IS reachable by name from
|
from the host: an app container that publishes no port. It IS reachable by name
|
||||||
another container on that network, which also exercises the real HTTP
|
from another container on that network, which also exercises the real
|
||||||
path - so this proves the app is serving, not merely that its
|
HTTP path -- so this proves the app is serving, not merely that its
|
||||||
database knows a version.
|
database knows a version.
|
||||||
|
|
||||||
Config, on the project's `verify` block:
|
Config, on the project's `verify` block:
|
||||||
|
|
||||||
"verify": {
|
"verify": {
|
||||||
"path": "/api/build-version",
|
"path": "/api/build-version",
|
||||||
"viaProxy": "edge_proxy_container",
|
"viaProxy": "evo_edge_proxy",
|
||||||
"upstream": "app_container:80"
|
"upstream": "myapp_app:80"
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns $null when either key is missing, so every project without
|
Returns $null when either key is missing, so every project without
|
||||||
this config keeps the behaviour it has today.
|
this config keeps exactly the behaviour it has today.
|
||||||
#>
|
#>
|
||||||
param($Proj)
|
param($Proj)
|
||||||
|
|
||||||
@ -370,9 +575,10 @@ function Get-LabelFromVersionJson {
|
|||||||
The build label out of a version endpoint's JSON text, or $null.
|
The build label out of a version endpoint's JSON text, or $null.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Apps disagree about the field name - some answer `build_version`,
|
Two field names in the fleet: one app answers `build_version` on
|
||||||
others `version`. Both mean "the build that is live", so both are
|
/api/build-version, another answers `version` on /health. Both mean
|
||||||
accepted rather than making an app rename its own field.
|
"the build that is live", so both are accepted rather than making an
|
||||||
|
app rename its own field.
|
||||||
#>
|
#>
|
||||||
param([string]$Text)
|
param([string]$Text)
|
||||||
|
|
||||||
@ -384,14 +590,50 @@ function Get-LabelFromVersionJson {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Get-LabelFromBuildJsonObj {
|
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)
|
param($obj)
|
||||||
if (-not $obj) { return $null }
|
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) {
|
if ($null -ne $obj.build -or $null -ne $obj.alpha) {
|
||||||
$alpha = if ($null -ne $obj.alpha) { [int]$obj.alpha } else { 1 }
|
$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)"
|
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}
|
# 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)"
|
return "v$([string]$obj.productVersion).$([int]$obj.buildNumber)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,3 +1074,85 @@ function Stop-ZTracking {
|
|||||||
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
|
||||||
Write-ZTrailer -FinalNote $FinalNote
|
Write-ZTrailer -FinalNote $FinalNote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Deploy-verification planning (pure; unit-tested in tests/) ──────────────
|
||||||
|
|
||||||
|
function Get-VerifyTimeout {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Seconds the live-build verification may wait, per project.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
verify.timeoutSeconds in zconfig.json lets a project that is slow to
|
||||||
|
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.
|
||||||
|
#>
|
||||||
|
param($Proj, [int]$DefaultSec)
|
||||||
|
if ($Proj.verify -and $Proj.verify.timeoutSeconds) {
|
||||||
|
return [int]$Proj.verify.timeoutSeconds
|
||||||
|
}
|
||||||
|
return $DefaultSec
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VerifyAttempts {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
The ordered ways to read this project's live build, most-trustworthy
|
||||||
|
first. Pure: config in, plan out - so the ordering rules are testable
|
||||||
|
without ssh.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
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
|
||||||
|
published port.
|
||||||
|
port - localhost:<verify.port> on the server. Same-box, still
|
||||||
|
unambiguous; used only if the body carries a version.
|
||||||
|
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 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.
|
||||||
|
|
||||||
|
The caller must walk this list EVERY retry, not once up front: the
|
||||||
|
verification runs while the app is restarting, which is exactly when
|
||||||
|
the good channels are briefly down. Deciding the channel before the
|
||||||
|
wait loop is how the whole window got spent on the worst one.
|
||||||
|
#>
|
||||||
|
param($Proj, [string]$ExecCmd)
|
||||||
|
$attempts = @()
|
||||||
|
if ($ExecCmd) {
|
||||||
|
$attempts += [pscustomobject]@{
|
||||||
|
Kind = 'exec'
|
||||||
|
Label = "docker network: $($Proj.verify.upstream) (via $($Proj.verify.viaProxy))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($Proj.verify -and $Proj.verify.port) {
|
||||||
|
$vPath = "/api/build-version"
|
||||||
|
if ($Proj.verify.path) { $vPath = [string]$Proj.verify.path }
|
||||||
|
$attempts += [pscustomobject]@{
|
||||||
|
Kind = 'port'
|
||||||
|
Port = [int]$Proj.verify.port
|
||||||
|
Path = $vPath
|
||||||
|
Label = "server localhost:$($Proj.verify.port)$vPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$verifyHost = $null
|
||||||
|
if ($Proj.deploy -and $Proj.deploy.verifyHost) { $verifyHost = [string]$Proj.deploy.verifyHost }
|
||||||
|
elseif ($Proj.domain) { $verifyHost = [string]$Proj.domain }
|
||||||
|
if ($verifyHost) {
|
||||||
|
$attempts += [pscustomobject]@{
|
||||||
|
Kind = 'edge'
|
||||||
|
HostHeader = $verifyHost
|
||||||
|
Label = "edge with Host: $verifyHost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# The comma stops PS 5.1 unrolling a one-element array into a bare
|
||||||
|
# object - the same pipeline trap that deadlocked ztests day 2.
|
||||||
|
return ,$attempts
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# ZKillOnly.ps1 — stop local dev server listeners for any project in zconfig.json.
|
# 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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json.
|
# ZKiller.ps1 — kill then restart dev servers for any project in zconfig.json.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"version": "v1.0.0.0.20"
|
"version": "v1.0.0.0.28"
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
releases/zscripts-v1.0.0.0.21.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.21.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.21.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.21.zip.sha256
Normal file
@ -0,0 +1 @@
|
|||||||
|
fa9d023d2641044ff154d12047d87a31250741e0699fc5b1360ab359bd68b5f0 zscripts-v1.0.0.0.21.zip
|
||||||
BIN
releases/zscripts-v1.0.0.0.22.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.22.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.22.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.22.zip.sha256
Normal file
@ -0,0 +1 @@
|
|||||||
|
446428c605d06ee500f58145a7915f7b7ee7482dbf60c5499f64436aaf945e86 zscripts-v1.0.0.0.22.zip
|
||||||
BIN
releases/zscripts-v1.0.0.0.23.zip
Normal file
BIN
releases/zscripts-v1.0.0.0.23.zip
Normal file
Binary file not shown.
1
releases/zscripts-v1.0.0.0.23.zip.sha256
Normal file
1
releases/zscripts-v1.0.0.0.23.zip.sha256
Normal file
@ -0,0 +1 @@
|
|||||||
|
27d6c8f0ea632c0ca940c7900d8f4682066b5963847b9838a474cc5d4cbb758f zscripts-v1.0.0.0.23.zip
|
||||||
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
|
||||||
113
scripts/plaintext_twins.py
Normal file
113
scripts/plaintext_twins.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Render this repo's markdown to plain-text twins with the markup removed.
|
||||||
|
|
||||||
|
The .txt files exist for terminals, pagers and anywhere markdown doesn't
|
||||||
|
render. They are generated - never edit one by hand:
|
||||||
|
|
||||||
|
python scripts/plaintext_twins.py # rewrite every .txt twin
|
||||||
|
python scripts/plaintext_twins.py --check # exit 1 if any is out of sync
|
||||||
|
|
||||||
|
tests/PlainTextTwins.Tests.ps1 runs --check, so a markdown edit that forgets
|
||||||
|
to regenerate fails the suite instead of shipping a twin that disagrees with
|
||||||
|
the file it mirrors.
|
||||||
|
|
||||||
|
Was readme_txt.py, which did README only. CHANGELOG.txt was kept by hand and
|
||||||
|
drifted the moment CHANGELOG.md was reorganised - and its docstring claimed a
|
||||||
|
--check the suite never actually ran. Both are fixed here: one renderer, every
|
||||||
|
pair, and a test that invokes it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
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:
|
||||||
|
text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text) # images -> alt text
|
||||||
|
text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1 (\2)", text) # links -> text (url)
|
||||||
|
text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text) # bold
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"\1", text) # italic
|
||||||
|
text = re.sub(r"`([^`]+)`", r"\1", text) # inline code
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def render(md: str) -> str:
|
||||||
|
out: list[str] = []
|
||||||
|
in_fence = False
|
||||||
|
for line in md.splitlines():
|
||||||
|
if line.lstrip().startswith("```"):
|
||||||
|
# Drop the fence markers; the code itself stays, indented so it
|
||||||
|
# still reads as a block without the backticks.
|
||||||
|
in_fence = not in_fence
|
||||||
|
continue
|
||||||
|
if in_fence:
|
||||||
|
out.append((" " + line) if line else "")
|
||||||
|
continue
|
||||||
|
# An HTML comment is invisible in rendered markdown, but its markers
|
||||||
|
# are not invisible in a text file - they read as stray punctuation.
|
||||||
|
# Keep what the comment says, drop the <!-- --> around it.
|
||||||
|
if line.strip() in ("<!--", "-->"):
|
||||||
|
continue
|
||||||
|
heading = re.match(r"^(#{1,6})\s+(.*)$", line)
|
||||||
|
if heading:
|
||||||
|
text = _inline(heading.group(2))
|
||||||
|
out.append(text)
|
||||||
|
out.append(("=" if len(heading.group(1)) == 1 else "-") * len(text))
|
||||||
|
continue
|
||||||
|
out.append(_inline(line))
|
||||||
|
text = "\n".join(out)
|
||||||
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||||
|
return text.strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
check = "--check" in sys.argv
|
||||||
|
stale: list[str] = []
|
||||||
|
for md_name, txt_name in PAIRS:
|
||||||
|
source = ROOT / md_name
|
||||||
|
if not source.exists():
|
||||||
|
print(f"{md_name} is missing - nothing to render")
|
||||||
|
return 1
|
||||||
|
rendered = render(source.read_text(encoding="utf-8"))
|
||||||
|
target = ROOT / txt_name
|
||||||
|
if check:
|
||||||
|
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
||||||
|
if current != rendered:
|
||||||
|
stale.append(txt_name)
|
||||||
|
continue
|
||||||
|
target.write_text(rendered, encoding="utf-8", newline="\n")
|
||||||
|
print(f"Wrote {target} ({len(rendered.splitlines())} lines)")
|
||||||
|
|
||||||
|
if check:
|
||||||
|
if stale:
|
||||||
|
print(f"out of sync: {', '.join(stale)}"
|
||||||
|
f" - run: python scripts/plaintext_twins.py")
|
||||||
|
return 1
|
||||||
|
print(f"in sync: {', '.join(t for _, t in PAIRS)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@ -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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# setup_backup_schedule.ps1 — create a scheduled task for daily backups + OneDrive sync
|
# 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
|
# Created by Kelly Michels - dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# 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
|
$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.
|
# 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 {
|
function Invoke-ZScript {
|
||||||
param([string]$Script, [string[]]$ScriptArgs = @())
|
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
|
$path = Join-Path $script:Install $Script
|
||||||
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" }
|
$out = & powershell -NoProfile -ExecutionPolicy Bypass -File $path @ScriptArgs 2>&1 | ForEach-Object { "$_" }
|
||||||
return [pscustomobject]@{
|
$result = [pscustomobject]@{
|
||||||
ExitCode = $LASTEXITCODE
|
ExitCode = $LASTEXITCODE
|
||||||
Output = ($out -join "`n")
|
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
|
# Created by Kelly Michels - dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# 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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# 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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
|
|||||||
73
tests/PlainTextTwins.Tests.ps1
Normal file
73
tests/PlainTextTwins.Tests.ps1
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# The .txt twins are generated, and this is what makes that true.
|
||||||
|
#
|
||||||
|
# Invoke-Pester .\tests
|
||||||
|
#
|
||||||
|
# scripts/plaintext_twins.py has always shipped a --check mode, and its
|
||||||
|
# docstring claimed "the test suite runs --check". Nothing ran it. So README.txt
|
||||||
|
# could drift from README.md silently, and CHANGELOG.txt - which no generator
|
||||||
|
# covered at all - actually did.
|
||||||
|
#
|
||||||
|
# A twin that disagrees with the file it mirrors is worse than no twin: it is a
|
||||||
|
# second document that looks authoritative and is wrong.
|
||||||
|
|
||||||
|
# -Skip is evaluated during DISCOVERY, before BeforeAll runs, so a python
|
||||||
|
# lookup done in BeforeAll leaves the flag $null and the real check silently
|
||||||
|
# skips - which is how this test first "passed" while verifying nothing.
|
||||||
|
BeforeDiscovery {
|
||||||
|
$script:Python = $null
|
||||||
|
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||||
|
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$script:Generator = Join-Path $script:RepoRoot "scripts\plaintext_twins.py"
|
||||||
|
|
||||||
|
$script:Python = $null
|
||||||
|
foreach ($candidate in @('python', 'python3', 'py')) {
|
||||||
|
$cmd = Get-Command $candidate -ErrorAction SilentlyContinue
|
||||||
|
if ($cmd) { $script:Python = $cmd.Source; break }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "plain-text twins" {
|
||||||
|
|
||||||
|
It "the generator is where the tests and the docs say it is" {
|
||||||
|
Test-Path -LiteralPath $script:Generator | Should -BeTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
$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)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It "every twin is in sync with its markdown" -Skip:(-not $script:Python) {
|
||||||
|
Push-Location $script:RepoRoot
|
||||||
|
try {
|
||||||
|
$output = & $script:Python $script:Generator --check 2>&1
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
}
|
||||||
|
finally { Pop-Location }
|
||||||
|
|
||||||
|
$code | Should -Be 0 -Because ($output -join "`n")
|
||||||
|
}
|
||||||
|
|
||||||
|
It "reports the python that was missing rather than passing quietly" -Skip:([bool]$script:Python) {
|
||||||
|
# Not a pass. If this is the test you are reading, --check never ran:
|
||||||
|
# install python, or regenerate the twins by hand before shipping.
|
||||||
|
Set-ItResult -Inconclusive -Because "no python on PATH, so the twins were not verified"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
@ -40,31 +40,18 @@ BeforeAll {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# name -> what it is, so a failure explains itself
|
# The rules live in sanitization-patterns.psd1, not here, so the
|
||||||
# pattern -> regex, case-insensitive
|
# publisher in the private tree can read the SAME list. It used to keep
|
||||||
# Kept narrow on purpose: "evomedia.net" alone is legitimate here (the
|
# its own, narrower one - secrets only, no identity rules - and therefore
|
||||||
# attribution header and the repo URL), so only the drive path and specific
|
# reported "clean" on files this suite rejects (evo.scripts#106).
|
||||||
# internal hosts are matched.
|
$patternFile = Join-Path $PSScriptRoot 'sanitization-patterns.psd1'
|
||||||
$script:Denied = @(
|
if (-not (Test-Path -LiteralPath $patternFile)) {
|
||||||
@{ Name = 'private project name'; Pattern = '\b(EvoCivilCode|EvoPlatform|DocketMail|SmartPlant\w*|ProvenSheet|evoehs|evoproven|evoaicc|evolocate|evoplatform)\b' }
|
throw "sanitization-patterns.psd1 is missing - the denylist has no source."
|
||||||
@{ 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' }
|
$script:Denied = (Import-PowerShellDataFile -LiteralPath $patternFile).Denied
|
||||||
@{ Name = 'local drive path'; Pattern = '[A-Za-z]:\\\\?evomedia\.net' }
|
if (-not $script:Denied -or $script:Denied.Count -eq 0) {
|
||||||
@{ Name = 'operator home path'; Pattern = '/home/ubuntu/' }
|
throw "sanitization-patterns.psd1 defined no rules - refusing to pass vacuously."
|
||||||
@{ Name = 'real pem key name'; Pattern = 'evomedia-prod\.pem' }
|
}
|
||||||
# RFC 5737 reserves 203.0.113.0/24 for documentation - that one is the
|
|
||||||
# correct placeholder and must stay allowed, as are loopback and the
|
|
||||||
# private ranges. Anything else that looks like a public IPv4 literal is
|
|
||||||
# suspect.
|
|
||||||
#
|
|
||||||
# The boundaries are [\d.] rather than \d on purpose: this toolkit's own
|
|
||||||
# 5-segment version (v1.0.0.0.14) contains "0.0.0.14", which a plain
|
|
||||||
# digit boundary happily reads as an address. Refusing a match that
|
|
||||||
# touches another dot rules out every version string without weakening
|
|
||||||
# detection of a real address, which is always delimited by whitespace
|
|
||||||
# or quotes.
|
|
||||||
@{ Name = 'non-documentation IP'; Pattern = '(?<![\d.])(?!203\.0\.113\.)(?!127\.0\.0\.1)(?!0\.0\.0\.0)(?!255\.)(?!10\.)(?!192\.168\.)(?!172\.(1[6-9]|2\d|3[01])\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?![\d.])' }
|
|
||||||
)
|
|
||||||
|
|
||||||
function Get-Hits {
|
function Get-Hits {
|
||||||
param([string]$Pattern)
|
param([string]$Pattern)
|
||||||
@ -102,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")"
|
$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" {
|
It "still detects a planted violation" {
|
||||||
# Mutation check. Without this the suite passes just as happily when the
|
# 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
|
# 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
96
tests/VerifyPlan.Tests.ps1
Normal file
96
tests/VerifyPlan.Tests.ps1
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Deploy-verification planning.
|
||||||
|
#
|
||||||
|
# Invoke-Pester .\tests
|
||||||
|
#
|
||||||
|
# The wrong-vhost failure was never about parsing a response - it was about
|
||||||
|
# WHICH channel got asked. So the channel-selection rules live in a pure
|
||||||
|
# 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 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.
|
||||||
|
|
||||||
|
BeforeAll {
|
||||||
|
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
||||||
|
|
||||||
|
function New-Proj {
|
||||||
|
param($Verify = $null, $Domain = $null, $Deploy = $null)
|
||||||
|
$p = [pscustomobject]@{}
|
||||||
|
if ($null -ne $Verify) { $p | Add-Member verify ([pscustomobject]$Verify) }
|
||||||
|
if ($null -ne $Domain) { $p | Add-Member domain $Domain }
|
||||||
|
if ($null -ne $Deploy) { $p | Add-Member deploy ([pscustomobject]$Deploy) }
|
||||||
|
return $p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Get-VerifyAttempts" {
|
||||||
|
|
||||||
|
It "puts the docker-network read first when configured" {
|
||||||
|
$proj = New-Proj -Verify @{ viaProxy = "evo_edge_proxy"; upstream = "app:80"; port = 8005 } -Domain "x.example"
|
||||||
|
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd "docker exec ..."
|
||||||
|
$attempts[0].Kind | Should -Be 'exec'
|
||||||
|
($attempts | ForEach-Object Kind) | Should -Be @('exec', 'port', 'edge')
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ..."
|
||||||
|
($attempts | ForEach-Object Kind) | Should -Not -Contain 'edge'
|
||||||
|
}
|
||||||
|
|
||||||
|
It "returns an empty plan when nothing trustworthy exists" {
|
||||||
|
# No verify config, no domain: the caller must SKIP, not guess.
|
||||||
|
$attempts = Get-VerifyAttempts -Proj (New-Proj) -ExecCmd ""
|
||||||
|
$attempts.Count | Should -Be 0
|
||||||
|
}
|
||||||
|
|
||||||
|
It "keeps the edge for a project with a domain, with its Host header" {
|
||||||
|
$proj = New-Proj -Domain "jwks.example"
|
||||||
|
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||||
|
$attempts.Count | Should -Be 1
|
||||||
|
$attempts[0].Kind | Should -Be 'edge'
|
||||||
|
$attempts[0].HostHeader | Should -Be "jwks.example"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "prefers deploy.verifyHost over domain for the edge Host header" {
|
||||||
|
$proj = New-Proj -Domain "old.example" -Deploy @{ verifyHost = "new.example" }
|
||||||
|
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||||
|
$attempts[0].HostHeader | Should -Be "new.example"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "carries the verify path into the port attempt, defaulting sensibly" {
|
||||||
|
$proj = New-Proj -Verify @{ port = 8005; path = "/health" }
|
||||||
|
(Get-VerifyAttempts -Proj $proj -ExecCmd "")[0].Path | Should -Be "/health"
|
||||||
|
$proj2 = New-Proj -Verify @{ port = 9000 }
|
||||||
|
(Get-VerifyAttempts -Proj $proj2 -ExecCmd "")[0].Path | Should -Be "/api/build-version"
|
||||||
|
}
|
||||||
|
|
||||||
|
It "survives the PS 5.1 one-element unroll" {
|
||||||
|
# A single attempt must still come back as something with .Count and
|
||||||
|
# index access - the pipeline trap that deadlocked ztests day 2.
|
||||||
|
$proj = New-Proj -Verify @{ port = 8005 }
|
||||||
|
$attempts = Get-VerifyAttempts -Proj $proj -ExecCmd ""
|
||||||
|
$attempts.Count | Should -Be 1
|
||||||
|
$attempts[0].Kind | Should -Be 'port'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Describe "Get-VerifyTimeout" {
|
||||||
|
|
||||||
|
It "uses the caller's default when the project says nothing" {
|
||||||
|
Get-VerifyTimeout -Proj (New-Proj) -DefaultSec 30 | Should -Be 30
|
||||||
|
}
|
||||||
|
|
||||||
|
It "lets a slow-booting project widen its own window" {
|
||||||
|
# An app that runs database migrations in its entrypoint exceeds
|
||||||
|
# 30s on every deploy that ships one, and a warning that fires on
|
||||||
|
# routine success trains people to ignore the real one.
|
||||||
|
$proj = New-Proj -Verify @{ viaProxy = "p"; upstream = "u"; timeoutSeconds = 120 }
|
||||||
|
Get-VerifyTimeout -Proj $proj -DefaultSec 30 | Should -Be 120
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
|
|
||||||
|
|||||||
58
tests/sanitization-patterns.psd1
Normal file
58
tests/sanitization-patterns.psd1
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<#
|
||||||
|
Patterns the PUBLIC repo must never contain.
|
||||||
|
|
||||||
|
Extracted from Sanitization.Tests.ps1 so that the test here and the
|
||||||
|
publisher in the private tree read ONE list instead of keeping two.
|
||||||
|
They had two, and they disagreed: the publisher's scan looked only for
|
||||||
|
secrets - keys, private-key blocks, ssh targets - while these rules are
|
||||||
|
about IDENTITY: internal project names, product domains, private-only
|
||||||
|
script names, operator paths.
|
||||||
|
|
||||||
|
So the publisher reported "clean" on files this suite rejects, and would
|
||||||
|
have published a tree that fails the public repo's own tests
|
||||||
|
(evo.scripts#106). One list, and that cannot drift apart again.
|
||||||
|
|
||||||
|
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 you publish. Add a pattern whenever a new
|
||||||
|
private identifier appears; a stale entry costs nothing.
|
||||||
|
|
||||||
|
Kept narrow on purpose: "evomedia.net" alone is legitimate here - the
|
||||||
|
attribution header and the repo URL both carry it - so only the drive
|
||||||
|
path and specific internal hosts are matched.
|
||||||
|
#>
|
||||||
|
@{
|
||||||
|
Denied = @(
|
||||||
|
# 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' }
|
||||||
|
# RFC 5737 reserves 203.0.113.0/24 for documentation - that one is the
|
||||||
|
# correct placeholder and must stay allowed, as are loopback and the
|
||||||
|
# private ranges. Anything else that looks like a public IPv4 literal is
|
||||||
|
# suspect.
|
||||||
|
#
|
||||||
|
# The boundaries are [\d.] rather than \d on purpose: this toolkit's own
|
||||||
|
# 5-segment version (v1.0.0.0.14) contains "0.0.0.14", which a plain
|
||||||
|
# digit boundary happily reads as an address. Refusing a match that
|
||||||
|
# touches another dot rules out every version string without weakening
|
||||||
|
# detection of a real address, which is always delimited by whitespace
|
||||||
|
# or quotes.
|
||||||
|
@{ Name = 'non-documentation IP'; Pattern = '(?<![\d.])(?!203\.0\.113\.)(?!127\.0\.0\.1)(?!0\.0\.0\.0)(?!255\.)(?!10\.)(?!192\.168\.)(?!172\.(1[6-9]|2\d|3[01])\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(?![\d.])' }
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# token-count.ps1 — measure script output volume to estimate AI agent token costs.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the
|
# zbackup.ps1 — local backups: zip project sources (plus a Postgres dump when the
|
||||||
# project's .env has a DATABASE_URL) into the backups folder.
|
# 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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zbackup_and_sync.ps1 — run backups, then sync the backups folder offsite.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zbackup_ec2.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zbackup_ec2.ps1 — pull backups down from the server: a Postgres dump for projects
|
# 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).
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zchecksums.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -54,6 +54,7 @@
|
|||||||
"deploy": {
|
"deploy": {
|
||||||
"zipName": "PyAppDeploy.zip",
|
"zipName": "PyAppDeploy.zip",
|
||||||
"gitPull": true,
|
"gitPull": true,
|
||||||
|
"tagOnDeploy": false,
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"docs"
|
"docs"
|
||||||
],
|
],
|
||||||
@ -121,6 +122,7 @@
|
|||||||
"analytics": {
|
"analytics": {
|
||||||
"label": "Analytics (any docker compose app)",
|
"label": "Analytics (any docker compose app)",
|
||||||
"kind": "docker",
|
"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",
|
"localRoot": "C:\\YourRoot\\analytics",
|
||||||
"domain": "analytics.yourdomain.com",
|
"domain": "analytics.yourdomain.com",
|
||||||
"remote": {
|
"remote": {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
||||||
|
|||||||
576
zdeploy.ps1
576
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zdeploy.ps1 — deploy any project defined in zconfig.json to the server.
|
# 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
|
# Each project runs its own docker compose stack; the handler is picked by the
|
||||||
@ -9,6 +9,9 @@
|
|||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# zdeploy <project> [<project> ...] [-Note "message"]
|
# 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 all # ztokens first, then every project (edge kinds next), stop at first failure
|
||||||
# zdeploy ztokens # refresh the live-usage stats (see below)
|
# zdeploy ztokens # refresh the live-usage stats (see below)
|
||||||
#
|
#
|
||||||
@ -18,12 +21,11 @@
|
|||||||
# zdeploy all -Note "weekly release"
|
# zdeploy all -Note "weekly release"
|
||||||
# zdeploy ztokens evo # refresh token-stats.json, then ship the site with it
|
# zdeploy ztokens evo # refresh token-stats.json, then ship the site with it
|
||||||
#
|
#
|
||||||
# "ztokens" is an OPTIONAL pseudo-project, not a zconfig entry: it runs
|
# "ztokens" is a pseudo-project, not a zconfig entry: it runs `ztokens -Publish`
|
||||||
# `ztokens -Publish` from a sibling ztokens checkout, if you have one, to
|
# from the sibling ztokens repo, refreshing the token-stats.json the public
|
||||||
# refresh a token-stats.json a site can chart. With no such checkout the step
|
# zscripts page charts. `all` runs it first automatically; called standalone,
|
||||||
# prints a skip and the rest of the run is unaffected. `all` runs it first
|
# list it before a site project (as above) so that project's deploy zip picks
|
||||||
# automatically; called standalone, list it before a site project (as above) so
|
# up the freshly written file.
|
||||||
# that project's deploy zip picks up the freshly written file.
|
|
||||||
#
|
#
|
||||||
# Flow (python/vite/nextjs): zip source -> free server disk space -> scp up ->
|
# Flow (python/vite/nextjs): zip source -> free server disk space -> scp up ->
|
||||||
# unzip into remote.path (preserving server-side .env* files and anything in
|
# unzip into remote.path (preserving server-side .env* files and anything in
|
||||||
@ -44,7 +46,13 @@
|
|||||||
param(
|
param(
|
||||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$Projects = @(),
|
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
@ -66,13 +74,170 @@ if (-not (Test-Path -LiteralPath $TempRoot)) {
|
|||||||
New-Item -ItemType Directory -Path $TempRoot -Force | Out-Null
|
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) {
|
if ($Projects.Count -eq 0) {
|
||||||
$keys = (Get-ZProjectKeys) -join ', '
|
$keys = (Get-ZProjectKeys) -join ', '
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Usage: zdeploy <project> [<project> ...] | all | ztokens [-Note `"message`"]" -ForegroundColor Yellow
|
Write-Host "Usage: zdeploy <project> [<project> ...] | all | ztokens [-Note `"message`"]" -ForegroundColor Yellow
|
||||||
Write-Host " Projects in zconfig.json: $keys" -ForegroundColor Gray
|
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 " 'all' deploys everything (edge kinds first) and stops at the first failure." -ForegroundColor Gray
|
||||||
Write-Host " 'ztokens' refreshes live-usage stats, if a sibling ztokens checkout exists." -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
|
Stop-ZTracking; exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +256,80 @@ if ($Projects -contains 'all') {
|
|||||||
# do the risky order, because this sort only ran for 'all'.
|
# do the risky order, because this sort only ran for 'all'.
|
||||||
# Order within each group is preserved, so an intentional sequence still holds
|
# Order within each group is preserved, so an intentional sequence still holds
|
||||||
# — notably `zdeploy ztokens evo`, where ztokens must still precede evo.
|
# — 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)
|
$requested = @($Projects)
|
||||||
$edgeKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -eq 'edge' })
|
$edgeKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -eq 'edge' })
|
||||||
$restKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -ne 'edge' })
|
$restKeys = @($Projects | Where-Object { $cfg.projects.$_.kind -ne 'edge' })
|
||||||
@ -102,6 +341,34 @@ if ($Projects.Count -gt 1) {
|
|||||||
Write-Host "Deploying: $($Projects -join ', ')$note" -ForegroundColor Cyan
|
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 {
|
function Get-DeployZipName {
|
||||||
param([string]$Key, $Proj)
|
param([string]$Key, $Proj)
|
||||||
if ($Proj.deploy -and $Proj.deploy.zipName) { return $Proj.deploy.zipName }
|
if ($Proj.deploy -and $Proj.deploy.zipName) { return $Proj.deploy.zipName }
|
||||||
@ -132,10 +399,12 @@ function Invoke-Ec2PreflightCleanup {
|
|||||||
"echo available_mb=`$avail_mb",
|
"echo available_mb=`$avail_mb",
|
||||||
"if [ `"`$avail_mb`" -lt 1500 ]; then echo 'ERROR: less than 1.5 GB free on /. Grow the root volume or run: sudo docker system prune -af' >&2; exit 11; fi"
|
"if [ `"`$avail_mb`" -lt 1500 ]; then echo 'ERROR: less than 1.5 GB free on /. Grow the root volume or run: sudo docker system prune -af' >&2; exit 11; fi"
|
||||||
) -join '; '
|
) -join '; '
|
||||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $preflightCmd
|
# Also through the wrapper (#119): the out-of-space branch above writes its
|
||||||
if ($LASTEXITCODE -ne 0) {
|
# ERROR to stderr and exits 11, so a bare ssh would surface a
|
||||||
throw "Server pre-flight cleanup failed (exit $LASTEXITCODE). Root volume too full (need ~1.5 GB free, ideally 3+)."
|
# NativeCommandError instead of the actionable message below - exactly when
|
||||||
}
|
# the operator most needs to be told what to do. -FailHint keeps it.
|
||||||
|
Invoke-Ec2Step "server pre-flight cleanup" $preflightCmd `
|
||||||
|
-FailHint "Root volume too full (need ~1.5 GB free, ideally 3+). Grow it or run: sudo docker system prune -af"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Post-deploy cleanup: prune build cache and dangling images created during this deploy.
|
# Post-deploy cleanup: prune build cache and dangling images created during this deploy.
|
||||||
@ -170,30 +439,96 @@ function Invoke-RemoteUnzip {
|
|||||||
Invoke-Ec2Step "unzip $ZipName" $bash
|
Invoke-Ec2Step "unzip $ZipName" $bash
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Operator-file preservation (issue #2) ────────────────────────────────────
|
# ── Operator-file preservation (issue #2, hardened in #107) ──────────────────
|
||||||
# Deploys replace the project directory wholesale, which used to destroy every
|
# Deploys replace the project directory wholesale, which used to destroy every
|
||||||
# operator-managed file except ./.env. These helpers preserve all .env* files
|
# operator-managed file except ./.env. These helpers preserve all .env* files
|
||||||
# at the project root PLUS any paths listed in deploy.preserve (files or
|
# at the project root PLUS any paths listed in deploy.preserve (files or
|
||||||
# directories), by tarring them to the home dir before the wipe and extracting
|
# directories), by tarring them to the home dir before the wipe and extracting
|
||||||
# them back after the unzip. Server-side copies win over anything shipped in
|
# them back after the unzip. Server-side copies win over anything shipped in
|
||||||
# the zip — the same semantics ./.env always had.
|
# the zip — the same semantics ./.env always had.
|
||||||
|
#
|
||||||
|
# WHY THE ARCHIVE IS TIMESTAMPED AND NEVER DELETED (#107)
|
||||||
|
# -------------------------------------------------------
|
||||||
|
# This used to write one fixed preserve_<key>.tgz, and preserve's FIRST action
|
||||||
|
# was `rm -f` on it. That is the opposite of safe. The window between
|
||||||
|
# `sudo rm -rf` on the project directory and the restore step is the only time
|
||||||
|
# the tarball is the sole copy of the production secrets - and an interrupted
|
||||||
|
# run (dropped ssh, exit 255) stops exactly there, leaving a perfect backup
|
||||||
|
# behind. The next run then deleted that backup before doing anything else,
|
||||||
|
# tarred a directory that no longer had the files, and reported success.
|
||||||
|
# `2>/dev/null; true` on the tar is what made it silent.
|
||||||
|
#
|
||||||
|
# That destroyed civilcode's production deploy/.env on 2026-08-30. The site
|
||||||
|
# survived only because the running container still held its environment; a
|
||||||
|
# restart would have made the loss permanent.
|
||||||
|
#
|
||||||
|
# So, three independent changes, any one of which would have prevented it:
|
||||||
|
#
|
||||||
|
# 1. Each run writes its own preserve_<key>_<stamp>.tgz and restore no
|
||||||
|
# longer deletes it. Nothing removes an archive that has not been
|
||||||
|
# superseded - retention below prunes old ones instead.
|
||||||
|
# 2. Preserve first extracts any earlier archives with `tar -k`, which fills
|
||||||
|
# in files a previous interrupted run lost WITHOUT overwriting anything
|
||||||
|
# currently on disk. A hand-repaired .env therefore wins over the stale
|
||||||
|
# copy in the archive.
|
||||||
|
# 3. Preserve refuses to continue if it captured nothing while an earlier
|
||||||
|
# archive for the same key did have contents. Capturing zero files is
|
||||||
|
# normal for a project with no operator files (edge, gitea, landing) and
|
||||||
|
# catastrophic for one that has them; the prior archive is what tells the
|
||||||
|
# difference.
|
||||||
|
#
|
||||||
|
# One stamp per zdeploy process, so preserve and restore agree on the filename
|
||||||
|
# without threading it through every call site.
|
||||||
|
$script:PreserveStamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||||
|
$script:PreserveKeep = 5
|
||||||
|
|
||||||
|
function Get-PreserveTarball {
|
||||||
|
param([string]$Key)
|
||||||
|
"$RemoteHome/preserve_${Key}_$($script:PreserveStamp).tgz"
|
||||||
|
}
|
||||||
|
|
||||||
function Save-OperatorFiles {
|
function Save-OperatorFiles {
|
||||||
param([string]$Key, $Proj, [string]$RemotePath)
|
param([string]$Key, $Proj, [string]$RemotePath)
|
||||||
$paths = @('.env*')
|
$paths = @('.env*')
|
||||||
if ($Proj.deploy -and $Proj.deploy.preserve) { $paths += @($Proj.deploy.preserve) }
|
if ($Proj.deploy -and $Proj.deploy.preserve) { $paths += @($Proj.deploy.preserve) }
|
||||||
$spec = $paths -join ' '
|
$spec = $paths -join ' '
|
||||||
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
$tarball = Get-PreserveTarball -Key $Key
|
||||||
# NOTE: no embedded quotes or $( ) here - PowerShell 5.1 strips embedded
|
$list = $tarball -replace '\.tgz$', '.list'
|
||||||
# double quotes when passing args to ssh.exe, silently corrupting the
|
$glob = "$RemoteHome/preserve_${Key}_*"
|
||||||
# remote command. Globs expand remotely; tar archives whatever exists
|
$drop = $script:PreserveKeep + 1
|
||||||
# and its nonzero exit for missing paths is deliberately swallowed.
|
|
||||||
Invoke-Ec2Step "preserve operator files ($spec)" "rm -f $tarball; cd $RemotePath && tar -czf $tarball $spec 2>/dev/null; true"
|
# NOTE: no embedded double quotes or $( ) here - PowerShell 5.1 strips
|
||||||
|
# embedded double quotes when passing args to ssh.exe, silently corrupting
|
||||||
|
# the remote command, and $( ) would be evaluated locally. Remote shell
|
||||||
|
# variables are backtick-escaped so PowerShell leaves them alone. Globs
|
||||||
|
# expand remotely; tar's nonzero exit for missing paths is swallowed, but
|
||||||
|
# an empty capture is NOT (see the guard below).
|
||||||
|
$bash =
|
||||||
|
"cd $RemotePath || exit 9; " +
|
||||||
|
"for t in ${glob}.tgz; do [ -e `$t ] && tar -xzkf `$t -C $RemotePath 2>/dev/null; done; true; " +
|
||||||
|
"tar -czf $tarball $spec 2>/dev/null; " +
|
||||||
|
"tar -tzf $tarball > $list 2>/dev/null; " +
|
||||||
|
"if [ ! -s $list ]; then " +
|
||||||
|
"for p in ${glob}.list; do " +
|
||||||
|
"if [ -s `$p ] && [ `$p != $list ]; then " +
|
||||||
|
"echo PRESERVE CAPTURED NOTHING BUT AN EARLIER ARCHIVE HAS FILES; exit 8; " +
|
||||||
|
"fi; " +
|
||||||
|
"done; " +
|
||||||
|
"fi; " +
|
||||||
|
"ls -1t ${glob}.tgz 2>/dev/null | tail -n +$drop | xargs -r rm -f; " +
|
||||||
|
"ls -1t ${glob}.list 2>/dev/null | tail -n +$drop | xargs -r rm -f; " +
|
||||||
|
"exit 0"
|
||||||
|
|
||||||
|
Invoke-Ec2Step "preserve operator files ($spec)" $bash `
|
||||||
|
-FailHint "Refusing to wipe $RemotePath - see $glob.tgz on the server."
|
||||||
}
|
}
|
||||||
|
|
||||||
function Restore-OperatorFiles {
|
function Restore-OperatorFiles {
|
||||||
param([string]$Key, [string]$RemotePath)
|
param([string]$Key, [string]$RemotePath)
|
||||||
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
$tarball = Get-PreserveTarball -Key $Key
|
||||||
Invoke-Ec2Step "restore operator files" "test -f $tarball && tar -xzf $tarball -C $RemotePath; rm -f $tarball; true"
|
# Overwrites, deliberately: server-side operator files beat whatever the
|
||||||
|
# zip shipped. The archive is left in place - see the header.
|
||||||
|
Invoke-Ec2Step "restore operator files" "test -f $tarball && tar -xzf $tarball -C $RemotePath; true"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Deploy verification (build-version match, not just HTTP 200 — a 200 can be
|
# ── Deploy verification (build-version match, not just HTTP 200 — a 200 can be
|
||||||
@ -211,6 +546,16 @@ function Wait-VerifyStaticBuild {
|
|||||||
# advances deliberately — one version bump per release — so "is the
|
# advances deliberately — one version bump per release — so "is the
|
||||||
# build I just packed live?" means an exact match.
|
# build I just packed live?" means an exact match.
|
||||||
$expectedLabel = Get-LabelFromBuildJsonObj $PreZipBuildState
|
$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
|
Write-Host "`n--- [$Key] Live build verification (expect $expectedLabel) ---" -ForegroundColor Cyan
|
||||||
$containerName = $Proj.remote.containerName
|
$containerName = $Proj.remote.containerName
|
||||||
$deadline = (Get-Date).AddSeconds(45)
|
$deadline = (Get-Date).AddSeconds(45)
|
||||||
@ -230,7 +575,7 @@ function Wait-VerifyStaticBuild {
|
|||||||
}
|
}
|
||||||
if ($r) {
|
if ($r) {
|
||||||
$remoteLabel = Get-LabelFromBuildJsonObj $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
|
Write-Host " PASS - live build $remoteLabel matches expected." -ForegroundColor Green
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -247,54 +592,87 @@ function Wait-VerifyStaticBuild {
|
|||||||
function Wait-VerifyApiBuild {
|
function Wait-VerifyApiBuild {
|
||||||
param([string]$Key, $Proj, [string]$ExpectedLabel, [int]$TimeoutSec = 60)
|
param([string]$Key, $Proj, [string]$ExpectedLabel, [int]$TimeoutSec = 60)
|
||||||
Write-Host "`n--- [$Key] Live build verification (expect $ExpectedLabel) ---" -ForegroundColor Cyan
|
Write-Host "`n--- [$Key] Live build verification (expect $ExpectedLabel) ---" -ForegroundColor Cyan
|
||||||
$headers = @{}
|
# deploy.verifyHost (or domain) decides which edge vhost may be asked;
|
||||||
# deploy.verifyHost overrides domain for verification only. The Host header
|
# both are consumed inside Get-VerifyAttempts now, where "no host at all"
|
||||||
# decides which edge vhost answers, and a project's public host can be
|
# excludes the edge channel entirely rather than defaulting to whatever
|
||||||
# deliberately unroutable while the app is perfectly healthy - that is why
|
# vhost the proxy serves (#101). verifyHost exists for a domain retired
|
||||||
# the override exists. Reach for it when a domain is being retired ahead of
|
# ahead of its replacement - a takedown once had a project's public host
|
||||||
# its replacement: the old host may be returning 410 while the new one has
|
# answering 410 while the app was healthy; no project sets it today.
|
||||||
# no DNS yet, so neither answers even though the app is fine.
|
# Every retry walks the channels in trust order - docker-network exec,
|
||||||
$verifyHost = if ($Proj.deploy -and $Proj.deploy.verifyHost) { $Proj.deploy.verifyHost } else { $Proj.domain }
|
# then localhost port, then (only with a Host to route by) the edge.
|
||||||
if ($verifyHost) { $headers['Host'] = $verifyHost }
|
# The choice used to be made ONCE, before the loop, by probing each
|
||||||
# Preferred when the project configures it: read the version from a
|
# channel - but the probes ran at the exact moment step [5] had
|
||||||
# container ON the shared docker network rather than through the public
|
# restarted the app, so both good channels were briefly down and the
|
||||||
# proxy. A service that publishes no port cannot be curled from the host
|
# whole window was spent on the edge. For a project with no domain that
|
||||||
# at all, and the proxy answers from whichever vhost matches the Host
|
# meant the default vhost: one project's check read a DIFFERENT
|
||||||
# header - so a container with no public route gets another site's
|
# project's build number, twice in a single day (#101). Re-resolving per
|
||||||
# version back. See Get-ServerSideVersionCommand.
|
# retry means the right channel is used the moment the app is back.
|
||||||
|
#
|
||||||
|
# The port channel still counts only when the body carries a version:
|
||||||
|
# one project's verify path is a JWKS endpoint - real, healthy, and no
|
||||||
|
# version in it - so it falls through to the edge (it has a domain),
|
||||||
|
# same as it always did.
|
||||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||||
$useExec = $false
|
$attempts = Get-VerifyAttempts -Proj $Proj -ExecCmd $execCmd
|
||||||
if ($execCmd) {
|
$TimeoutSec = Get-VerifyTimeout -Proj $Proj -DefaultSec $TimeoutSec
|
||||||
$probe = (ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd | Out-String).Trim()
|
if ($attempts.Count -eq 0) {
|
||||||
if (Get-LabelFromVersionJson $probe) { $useExec = $true }
|
# No trustworthy channel exists: no viaProxy, no port, no host to
|
||||||
}
|
# route an edge request by. Asking the edge anyway can only reach
|
||||||
if ($useExec) {
|
# the DEFAULT vhost - a different product - and a check that can
|
||||||
Write-Host " Asking on server: $($Proj.verify.upstream) (via $($Proj.verify.viaProxy))" -ForegroundColor DarkGray
|
# only ever read someone else's number is worse than no check.
|
||||||
|
Write-Host " SKIPPED: no way to verify this project without reading the wrong vhost - configure verify.viaProxy/port, or a domain (#101)." -ForegroundColor Yellow
|
||||||
|
return $false
|
||||||
}
|
}
|
||||||
|
Write-Host " Channels, in order: $(($attempts | ForEach-Object { $_.Label }) -join '; ')" -ForegroundColor DarkGray
|
||||||
|
|
||||||
|
$sawVersion = $false
|
||||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||||
while ((Get-Date) -lt $deadline) {
|
while ((Get-Date) -lt $deadline) {
|
||||||
|
foreach ($attempt in $attempts) {
|
||||||
|
$r = $null
|
||||||
try {
|
try {
|
||||||
if ($useExec) {
|
switch ($attempt.Kind) {
|
||||||
|
'exec' {
|
||||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd
|
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd
|
||||||
$live = Get-LabelFromVersionJson ($raw | Out-String)
|
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
||||||
} else {
|
|
||||||
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
|
||||||
$live = if ($r -and $r.build_version) { [string]$r.build_version } else { $null }
|
|
||||||
}
|
}
|
||||||
if ($live) {
|
'port' {
|
||||||
if ($live -eq $ExpectedLabel) {
|
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "curl -s -m 8 http://localhost:$($attempt.Port)$($attempt.Path)"
|
||||||
Write-Host " PASS - live build $live matches expected." -ForegroundColor Green
|
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
||||||
return $true
|
}
|
||||||
|
'edge' {
|
||||||
|
$edgeHeaders = @{ 'Host' = $attempt.HostHeader }
|
||||||
|
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $edgeHeaders -TimeoutSec 10 -ErrorAction Stop
|
||||||
}
|
}
|
||||||
Write-Host " Live build is $live, expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host " version endpoint not ready yet - waiting..." -ForegroundColor DarkGray
|
continue # channel not ready; the next one gets its turn
|
||||||
|
}
|
||||||
|
if ($null -eq $r) { continue }
|
||||||
|
# Two field names in the fleet: some apps answer build_version
|
||||||
|
# on /api/build-version, others answer version on /health. Both
|
||||||
|
# are "the build that is live", so accept either rather than
|
||||||
|
# making every app rename its own field.
|
||||||
|
$live = if ($r.build_version) { [string]$r.build_version } elseif ($r.version) { [string]$r.version } else { $null }
|
||||||
|
if (-not $live) { continue } # answered, but not about versions (JWKS etc.)
|
||||||
|
$sawVersion = $true
|
||||||
|
if ($live -eq $ExpectedLabel) {
|
||||||
|
Write-Host " PASS - live build $live matches expected ($($attempt.Label))." -ForegroundColor Green
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
Write-Host " Live build is $live via $($attempt.Label), expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
||||||
|
break # one wrong-version read this pass is enough; retry after the sleep
|
||||||
}
|
}
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
}
|
}
|
||||||
Write-Host " WARNING: live build did not match $ExpectedLabel within ${TimeoutSec}s (a stale build may be cached)." -ForegroundColor Yellow
|
# Say which failure this actually was: a version that never matched is a
|
||||||
|
# stale/failed build; channels that never answered is "could not verify",
|
||||||
|
# and pretending otherwise is how a warning gets ignored.
|
||||||
|
if ($sawVersion) {
|
||||||
|
Write-Host " WARNING: live build did not match $ExpectedLabel within ${TimeoutSec}s (upload or Docker build may have failed, or a stale build is cached)." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " WARNING: could not verify within ${TimeoutSec}s - no channel answered with a version (app may still be starting; raise verify.timeoutSeconds if this project boots slowly)." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -407,7 +785,7 @@ function Invoke-PythonDeploy {
|
|||||||
Invoke-Ec2Step "require compose directory" "test -d $composeDir"
|
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 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 "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) {
|
if ($hasVersionTool) {
|
||||||
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
||||||
@ -435,7 +813,7 @@ function Invoke-PythonDeploy {
|
|||||||
# in the container, is written to .build_version below, and is
|
# in the container, is written to .build_version below, and is
|
||||||
# proven by the /api/build-version check — which is the thing that
|
# proven by the /api/build-version check — which is the thing that
|
||||||
# actually establishes what is deployed.
|
# actually establishes what is deployed.
|
||||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "echo '$BuildVersion' | sudo tee $remotePath/.build_version > /dev/null"
|
Invoke-Ec2Step "record the live build number" "echo '$BuildVersion' | sudo tee $remotePath/.build_version > /dev/null"
|
||||||
$changelogTool = Join-Path $root "scripts\build_changelog_tool.py"
|
$changelogTool = Join-Path $root "scripts\build_changelog_tool.py"
|
||||||
if (Test-Path -LiteralPath $changelogTool) {
|
if (Test-Path -LiteralPath $changelogTool) {
|
||||||
if ([string]::IsNullOrWhiteSpace($ChangeNote)) { $ChangeNote = "Build deployed" }
|
if ([string]::IsNullOrWhiteSpace($ChangeNote)) { $ChangeNote = "Build deployed" }
|
||||||
@ -443,10 +821,24 @@ function Invoke-PythonDeploy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "`n--- [5] Restarting app to pick up new version ---" -ForegroundColor Cyan
|
Write-Host "`n--- [5] Restarting app to pick up new version ---" -ForegroundColor Cyan
|
||||||
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "cd $composeDir && sudo COMPOSE_BAKE=false docker compose restart $appSvc"
|
# Through Invoke-Ec2Step, not a bare ssh: `docker compose restart`
|
||||||
if ($LASTEXITCODE -ne 0) { throw "App restart after build bump failed (exit $LASTEXITCODE)" }
|
# writes " Container <name> Restarting" to STDERR as ordinary
|
||||||
|
# progress, and under ErrorActionPreference='Stop' PS 5.1 turns any
|
||||||
|
# native stderr line into a terminating NativeCommandError whatever
|
||||||
|
# the exit code. That threw here on a deploy that had fully
|
||||||
|
# succeeded - and it threw BEFORE Wait-VerifyApiBuild, so the step
|
||||||
|
# that proves what is actually deployed never ran (#119). The
|
||||||
|
# wrapper flattens stderr and judges by exit code alone, and throws
|
||||||
|
# on non-zero itself, so the hand-written check is gone with it.
|
||||||
|
Invoke-Ec2Step "restart $appSvc to pick up the new build" `
|
||||||
|
"cd $composeDir && sudo COMPOSE_BAKE=false docker compose restart $appSvc" `
|
||||||
|
-FailHint "App restart after the build bump failed."
|
||||||
|
|
||||||
Wait-VerifyApiBuild -Key $Key -Proj $Proj -ExpectedLabel $BuildVersion -TimeoutSec 30 | Out-Null
|
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) {
|
} elseif ($Proj.verify -and $Proj.verify.port) {
|
||||||
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
||||||
} elseif ($Proj.domain) {
|
} elseif ($Proj.domain) {
|
||||||
@ -537,7 +929,7 @@ function Invoke-ViteDeploy {
|
|||||||
if ($edgeProj -and $edgeProj.Config.proxyContainer) {
|
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 "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
|
Wait-VerifyStaticBuild -Key $Key -Proj $Proj -PreZipBuildState $preZipBuild
|
||||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||||
@ -646,7 +1038,7 @@ function Invoke-NextDeploy {
|
|||||||
# offline for the whole build - minutes for a Next.js app - and left it
|
# offline for the whole build - minutes for a Next.js app - and left it
|
||||||
# offline if the build failed. That is not hypothetical: one deploy
|
# offline if the build failed. That is not hypothetical: one deploy
|
||||||
# stopped the stack, the build did not finish, and the site served 502
|
# stopped the stack, the build did not finish, and the site served 502
|
||||||
# for 19 hours with no container running at all. The
|
# for 19 hours with no container at all. The
|
||||||
# old image keeps serving while the new one builds, so a failed build is
|
# old image keeps serving while the new one builds, so a failed build is
|
||||||
# now harmless and the outage is the seconds between down and up.
|
# now harmless and the outage is the seconds between down and up.
|
||||||
#
|
#
|
||||||
@ -663,14 +1055,14 @@ function Invoke-NextDeploy {
|
|||||||
if ($Proj.migrations -eq "prisma") {
|
if ($Proj.migrations -eq "prisma") {
|
||||||
Invoke-Ec2Step "apply prisma migrations" "cd $composeDir && sudo docker compose exec -T $appSvc npx prisma migrate deploy"
|
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
|
Write-Host "`n--- [5] Verifying deployment ---" -ForegroundColor Cyan
|
||||||
# Same precedence as the python handler. Do NOT probe http://<ec2-ip>:<prod-port>/
|
# Same precedence as the python handler. Do NOT probe http://<ec2-ip>:<prod-port>/
|
||||||
# here: a compose stack behind the edge proxy usually publishes to
|
# here: a compose stack behind the edge proxy usually publishes to
|
||||||
# 127.0.0.1 only, so that probe can never answer and the old "is the port
|
# 127.0.0.1 only, so that probe can never answer and the old "is the port
|
||||||
# open in the security group?" warning sent you chasing a firewall rule
|
# open in the security group?" warning sent you chasing a firewall rule
|
||||||
# for an app that was already up.
|
# for an app that was already up. See issue #28.
|
||||||
if ($Proj.verify -and $Proj.verify.port) {
|
if ($Proj.verify -and $Proj.verify.port) {
|
||||||
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
Test-DeployHealth -Key $Key -Proj $Proj -TimeoutSec 60 | Out-Null
|
||||||
} elseif ($Proj.domain) {
|
} elseif ($Proj.domain) {
|
||||||
@ -752,10 +1144,11 @@ function Invoke-EdgeDeploy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Content subdirectories the proxy serves (fonts/, vendor/, ...) ship too —
|
# Content subdirectories the proxy serves (fonts/, vendor/, ...) ship too —
|
||||||
# only server-side state stays put. Skipping them is how self-hosted assets
|
# only server-side state stays put. Skipping them is how the self-hosted
|
||||||
# silently never reach prod: docker creates empty mount-point dirs and nginx
|
# Chart.js and fonts silently never reached prod (charts rendered blank).
|
||||||
# serves 404s from them, so fonts fall back and vendored JS never loads.
|
# .pytest_cache is a local test artifact, already gitignored; it has no
|
||||||
$skipDirs = @('nginx-logs', '.git')
|
# business on the proxy box and only adds noise to the upload log.
|
||||||
|
$skipDirs = @('nginx-logs', '.git', '.pytest_cache')
|
||||||
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
||||||
foreach ($d in $dirs) {
|
foreach ($d in $dirs) {
|
||||||
Write-Host " >> uploading $($d.Name)/ (recursive)" -ForegroundColor DarkCyan
|
Write-Host " >> uploading $($d.Name)/ (recursive)" -ForegroundColor DarkCyan
|
||||||
@ -775,15 +1168,16 @@ function Invoke-EdgeDeploy {
|
|||||||
Invoke-Ec2Step "edge nginx reload" "sudo docker exec $pc nginx -s reload || true"
|
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 "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
|
Write-Host "--- [Done] Edge proxy deploy finished ---" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-StaticDeploy {
|
function Invoke-StaticDeploy {
|
||||||
param([string]$Key, $Proj)
|
param([string]$Key, $Proj)
|
||||||
|
|
||||||
# Plain static sites - no build, no container of their own. A shared web
|
# Plain static sites - no build, no container of their own. The landing
|
||||||
# container serves them straight off disk, so shipping the files IS the
|
# container serves them straight off disk, one directory per host, so
|
||||||
# deploy: there is nothing to restart afterwards.
|
# shipping the files IS the deploy: there is nothing to restart afterwards.
|
||||||
$root = Join-Path $Proj.localRoot $Proj.siteDir
|
$root = Join-Path $Proj.localRoot $Proj.siteDir
|
||||||
$remotePath = $Proj.remote.path
|
$remotePath = $Proj.remote.path
|
||||||
|
|
||||||
@ -801,11 +1195,8 @@ function Invoke-StaticDeploy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# A large media file uploaded in place is served half-written to anyone who
|
# A large media file uploaded in place is served half-written to anyone who
|
||||||
# requests it mid-copy. Ship each directory to a sibling, then swap it in -
|
# requests it mid-copy. Ship the directory to a sibling, then swap it in.
|
||||||
# the swap is a rename, so the switch is atomic and visitors never see a
|
$skipDirs = @('.git', '.pytest_cache', 'node_modules')
|
||||||
# partial file.
|
|
||||||
$skipDirs = @($script:JunkDirNames) + @('.github')
|
|
||||||
if ($Proj.deploy -and $Proj.deploy.skipDirs) { $skipDirs += @($Proj.deploy.skipDirs) }
|
|
||||||
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
||||||
foreach ($d in $dirs) {
|
foreach ($d in $dirs) {
|
||||||
Write-Host " >> uploading $($d.Name)/ (recursive, staged)" -ForegroundColor DarkCyan
|
Write-Host " >> uploading $($d.Name)/ (recursive, staged)" -ForegroundColor DarkCyan
|
||||||
@ -815,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 "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
|
Write-Host "--- [Done] Static site deploy finished ---" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -862,19 +1254,22 @@ function Invoke-DockerDeploy {
|
|||||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
|
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-Ec2Step "docker compose up -d" "cd $remotePath && sudo docker compose up -d"
|
||||||
|
|
||||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
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-Host "`n--- [Done] $($Proj.label) deploy finished ---" -ForegroundColor Green
|
||||||
Write-DeployLocation -Proj $Proj
|
Write-DeployLocation -Proj $Proj
|
||||||
}
|
}
|
||||||
|
|
||||||
# Pseudo-project "ztokens": not a zconfig entry, no compose stack, and entirely
|
# Pseudo-project "ztokens": not a zconfig entry, no compose stack. Runs
|
||||||
# optional. Runs `ztokens -Publish` from a sibling ztokens checkout so a site
|
# `ztokens -Publish` from the sibling ztokens repo so the public zscripts page
|
||||||
# that charts usage data has something current to ship. Missing checkout, or a
|
# has current data. A failure here warns rather than aborting the rest of the
|
||||||
# failure, warns rather than aborting the rest of the deploy list - it is a
|
# deploy list - it's a nice-to-have refresh, not a deploy step.
|
||||||
# nice-to-have refresh, not a deploy step.
|
|
||||||
function Invoke-ZTokensPublish {
|
function Invoke-ZTokensPublish {
|
||||||
Write-Host "`n=== ztokens: refreshing live-usage stats ===" -ForegroundColor Cyan
|
Write-Host "`n=== ztokens: refreshing live-usage stats ===" -ForegroundColor Cyan
|
||||||
$ztokensScript = Join-Path (Split-Path -Parent $PSScriptRoot) "ztokens\ztokens.ps1"
|
$ztokensScript = Join-Path (Split-Path -Parent $PSScriptRoot) "ztokens\ztokens.ps1"
|
||||||
@ -909,6 +1304,16 @@ function Invoke-ZTokensPublish {
|
|||||||
|
|
||||||
# ── Dispatch ─────────────────────────────────────────────────────────────────
|
# ── 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) {
|
foreach ($key in $Projects) {
|
||||||
# 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old
|
# 'ztokens' matches the tool it runs (ztokens.cmd / ztokens.ps1). The old
|
||||||
# singular 'ztoken' still works so existing habits and any script that
|
# singular 'ztoken' still works so existing habits and any script that
|
||||||
@ -926,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." }
|
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 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
|
# 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
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
# zec2.ps1 — quick reachability check (TCP + HTTP + live build version) for deployed projects.
|
||||||
#
|
#
|
||||||
@ -22,9 +22,11 @@ Start-ZTracking
|
|||||||
|
|
||||||
$cfg = Get-ZConfig
|
$cfg = Get-ZConfig
|
||||||
if (-not $HostName) { $HostName = $cfg.ec2.ip }
|
if (-not $HostName) { $HostName = $cfg.ec2.ip }
|
||||||
# Needed by the container-side version read below; same names zec2online.ps1
|
# Needed by the container-side version read below. zec2 had no ssh of its own
|
||||||
# uses. Without them that read throws inside its try/catch and falls through
|
# before that, so the read referenced three variables this script never
|
||||||
# silently, which looks identical to a service that cannot be reached.
|
# 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
|
$PemKey = $cfg.ec2.pemKey
|
||||||
$SshTarget = Get-Ec2Target
|
$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
|
$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 }
|
if ($r) { Write-Host " Live build: $(Get-LabelFromBuildJsonObj $r)" -ForegroundColor Gray }
|
||||||
} else {
|
} else {
|
||||||
# Container-side first where the project configures it: a build
|
# Container-side first where the project configures it: the
|
||||||
# stamp is not public on every site, and asking the proxy answers
|
# endpoint is not public on every project, and asking the edge
|
||||||
# from whichever vhost matches the Host header.
|
# answers from whichever vhost matches the Host header.
|
||||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||||
$label = $null
|
$label = $null
|
||||||
if ($execCmd -and (Test-Path $PemKey)) {
|
if ($execCmd -and (Test-Path $PemKey)) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2_rotatekeys.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zec2_rotatekeys.ps1 - rotate / reset secret keys in a project's SERVER-SIDE
|
# 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
|
# .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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2online.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zec2online.ps1 — deep health check: verify apps are live AND running the expected
|
# zec2online.ps1 — deep health check: verify apps are live AND running the expected
|
||||||
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
# build; auto-start downed stacks via docker compose and stream diagnostics.
|
||||||
@ -86,9 +86,10 @@ function Get-RemoteVersionLabel {
|
|||||||
param($Proj)
|
param($Proj)
|
||||||
$headers = @{}
|
$headers = @{}
|
||||||
if ($Proj.domain) { $headers['Host'] = $Proj.domain }
|
if ($Proj.domain) { $headers['Host'] = $Proj.domain }
|
||||||
# Container-side first where the project configures it. A build stamp is
|
# Container-side first where the project configures it. The endpoint is
|
||||||
# not public on every site, and the proxy answers from whichever vhost
|
# not public on every project, and the edge answers from whichever vhost
|
||||||
# matches the Host header - which is how a check reads another service.
|
# matches the Host header -- which is how a check reads another
|
||||||
|
# product's version.
|
||||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
||||||
if ($execCmd -and (Test-Path $PemKey)) {
|
if ($execCmd -and (Test-Path $PemKey)) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKillOnly.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through.
|
# zkill.ps1 — alias for ZKillOnly.ps1 (kept so both names work). All args pass through.
|
||||||
& (Join-Path $PSScriptRoot "ZKillOnly.ps1") @args
|
& (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 Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zrelease.ps1 - package the current version as a downloadable zip.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrepair.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zrepair.ps1 — audit and repair container/proxy routing on the server, then smoke test.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through.
|
# zrestart.ps1 — alias for ZKiller.ps1 (kept so both names work). All args pass through.
|
||||||
& (Join-Path $PSScriptRoot "ZKiller.ps1") @args
|
& (Join-Path $PSScriptRoot "ZKiller.ps1") @args
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZKiller.ps1" -Detached %*
|
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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsetup.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zsetup.ps1 — prepare a project for local dev: create its Python venv and install
|
# zsetup.ps1 — prepare a project for local dev: create its Python venv and install
|
||||||
# dependencies (python kind), or run `npm install` (vite/nextjs). Idempotent -
|
# 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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zsetup_mail.ps1 — create admin@ and noreply@ mailboxes in a docker-mailserver
|
# 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.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zstart.ps1 — start local dev servers for any project defined in zconfig.json.
|
# 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
|
Write-Host " env $($item.Name)=$($item.Value)" -ForegroundColor DarkGray
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($Proj.start.gitPull -and (Test-Path (Join-Path $Proj.localRoot ".git"))) {
|
if ($Proj.start.gitPull) {
|
||||||
Push-Location -LiteralPath $Proj.localRoot
|
# Never lets a pull stand between the user and a running server: the
|
||||||
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
# helper reports, it does not throw (#130). The inline version this
|
||||||
# instead of blocking the server start on a "Username for ..." prompt.
|
# replaced aborted on git's ordinary stderr progress under Stop.
|
||||||
$prev = $env:GIT_TERMINAL_PROMPT; $env:GIT_TERMINAL_PROMPT = "0"
|
$pull = Invoke-StartGitPull -Root $Proj.localRoot
|
||||||
try {
|
if ($pull.Ok) {
|
||||||
$pullOut = git pull --ff-only 2>&1
|
Write-Host " git pull: $($pull.Message)" -ForegroundColor DarkGray
|
||||||
$last = ($pullOut | Select-Object -Last 1)
|
} else {
|
||||||
Write-Host " git pull: $last" -ForegroundColor DarkGray
|
Write-Host " Auto-pull skipped - starting with the current checkout. ($($pull.Message); set start.gitPull=false to stop trying)" -ForegroundColor Yellow
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
REM Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zstart_docker.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zstart_docker.ps1 — bring up a local docker compose stack from <scriptsRoot>\docker\.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0ZStart.ps1" -Detached %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zstop.ps1 — stop docker compose stacks on the server without removing data or files.
|
# 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 Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zsync.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zsync.ps1 — copy new backup files offsite; or build + mirror a vite project's dist.
|
# 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 Evomedia.net — https://github.com/evomedia-net/evo.zscripts
|
||||||
REM Created by Kelly Michels · dev@evomedia.net
|
REM Created by Kelly Michels · dev@evomedia.net
|
||||||
REM Licensed under the MIT License. See LICENSE.
|
REM Licensed under the MIT License. See LICENSE.
|
||||||
REM Version: v1.0.0.0.20
|
REM Version: v1.0.0.0.28
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*
|
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
|
# Created by Kelly Michels · dev@evomedia.net
|
||||||
# Licensed under the MIT License. See LICENSE.
|
# Licensed under the MIT License. See LICENSE.
|
||||||
# Version: v1.0.0.0.20
|
# Version: v1.0.0.0.28
|
||||||
|
|
||||||
# zversion.ps1 - manage the toolkit version.
|
# zversion.ps1 - manage the toolkit version.
|
||||||
#
|
#
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user