mirror of
https://github.com/kellymichels/zscripts-token-savers
synced 2026-10-08 07:28:18 +00:00
Compare commits
No commits in common. "master" and "v1.0.0.0.14" have entirely different histories.
master
...
v1.0.0.0.1
14
.gitattributes
vendored
14
.gitattributes
vendored
@ -4,18 +4,8 @@
|
|||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
*.cmd text eol=crlf
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
# Every .txt here is either a generated twin or a manifest, and all of them
|
# The checksum manifest must stay LF: `sha256sum -c` treats a trailing CR as
|
||||||
# want LF. plaintext_twins.py writes LF and git stores LF, but without this
|
# part of the filename and reports every entry as missing.
|
||||||
# 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
51
.github/workflows/rerun-timed-out.yml
vendored
@ -1,51 +0,0 @@
|
|||||||
# 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
80
.github/workflows/tests.yml
vendored
@ -1,80 +0,0 @@
|
|||||||
# 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,6 +19,3 @@ md/
|
|||||||
|
|
||||||
# Pester coverage output (regenerated; never committed)
|
# Pester coverage output (regenerated; never committed)
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
# Generated by scripts/plaintext_twins.py
|
|
||||||
__pycache__/
|
|
||||||
|
|||||||
243
CHANGELOG.md
243
CHANGELOG.md
@ -1,215 +1,15 @@
|
|||||||
<!--
|
<!--
|
||||||
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
|
|
||||||
- **`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
|
|
||||||
written.
|
|
||||||
|
|
||||||
## v1.0.0.0.19 - 2026-08-30
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- **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
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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
|
||||||
@ -223,8 +23,6 @@ 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
|
||||||
@ -236,19 +34,16 @@ 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
|
||||||
@ -257,8 +52,15 @@ 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) —
|
||||||
## v1.0.0.0.0 - 2026-07-28
|
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.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
- **Versioned releases: `zversion`, `zrelease`, `releases/`** — the toolkit now
|
||||||
@ -272,7 +74,6 @@ 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
|
||||||
@ -283,18 +84,15 @@ 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**
|
||||||
@ -307,12 +105,10 @@ 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
|
||||||
@ -320,19 +116,16 @@ 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.
|
||||||
@ -343,33 +136,19 @@ 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),
|
||||||
|
|||||||
270
CHANGELOG.txt
270
CHANGELOG.txt
@ -1,240 +1,15 @@
|
|||||||
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
|
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
|
|
||||||
written.
|
|
||||||
|
|
||||||
v1.0.0.0.19 - 2026-08-30
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
Added
|
|
||||||
-----
|
|
||||||
- **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
|
|
||||||
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.
|
|
||||||
|
|
||||||
v1.0.0.0.14 - 2026-08-20
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
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
|
||||||
@ -247,8 +22,6 @@ 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
|
||||||
-----
|
-----
|
||||||
@ -262,19 +35,16 @@ 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
|
||||||
@ -283,9 +53,15 @@ 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) —
|
||||||
v1.0.0.0.0 - 2026-07-28
|
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.
|
||||||
|
|
||||||
Added
|
Added
|
||||||
-----
|
-----
|
||||||
@ -300,7 +76,6 @@ 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
|
||||||
@ -311,18 +86,15 @@ 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
|
||||||
@ -335,12 +107,10 @@ 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
|
||||||
@ -348,19 +118,16 @@ 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.
|
||||||
@ -372,34 +139,19 @@ 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,45 +1,42 @@
|
|||||||
6e95736007b3906950d95a830705ac1118ebfc11836d2c717d6dd49cb3157966 setup_backup_schedule.ps1
|
66ed14be538677f84983d9a3f474b3d4293d90ff9e64e172c7ebc52a7ff54b13 setup_backup_schedule.ps1
|
||||||
d6ab2ddb273a8ca870bd0673cffdd0907f16f4718367b4413a3e34bb1f1769c8 token-count.ps1
|
51a29d81dd5b8792a2262e8977b37a0db3172751ed510507ff6579e625837efd token-count.ps1
|
||||||
30ccf43c371ae95cecd5b443d9214ac8ea71ec83be94bc15bcf359beefb8dee1 zbackup.cmd
|
a8a0e4449750df0599424608766d871fca1462d85086f82eec5a9e0d02545ad5 zbackup.cmd
|
||||||
b3549b346c90a8ae5a05c4c91b7ac370f72467c4cb0d019edcd0ea80b994393e zbackup.ps1
|
272f14751b7e5976a1c33024df57c237cb48c74fd44bc64e144f09700eaef493 zbackup.ps1
|
||||||
b6e5e15f8e2b128219c7b8b9db3e9b7804eee60fd6aed43f184210048e518f3c zbackup_and_sync.ps1
|
22269ebf3db51ff82324bc56b529bd8b28708a8a52457fb041b2147f1883c072 zbackup_and_sync.ps1
|
||||||
b407ef5b298cd6fe94af492e9254b4447e34333d22f8c29d3e9d4d2881651cf3 zbackup_ec2.cmd
|
d03d5adfbae02afaa158c76a9e8d22e0a3e4a0260b5d5b4707e6f6450f764d01 zbackup_ec2.cmd
|
||||||
55df5f2e19e81317b1e33b7b030b252fc4d6f49c0c77229d9e4e86affeb3b8ef zbackup_ec2.ps1
|
dbd1b9c64fba16a308ffa8fa936f1bfda556b049aaa30ebe5400ca74b7e6aa5d zbackup_ec2.ps1
|
||||||
64bc3148b09e422ffe4890339fade2354a31f2c7ba004f2728c29a0a39c32f08 zchecksums.cmd
|
e03075f367a9ecee0f97438bc381044c74b7bd4e8cb4ff66b40048bba7ffd25a zchecksums.cmd
|
||||||
3e1c573e446238cae494d22d25376f278333dc25fdbc0a5abbdaa9349722b438 zchecksums.ps1
|
8382ad5f972405678b73616f608a80e3eb1814c927ba104e446d36c4f9f5c66d zchecksums.ps1
|
||||||
049f8e79fc8c3d8d96ae8ccb1155d191e2dbf2753f4d4d73ceca00aa1ab0481b zdeploy.cmd
|
3d1064817ace57fe61c54104900775a0208fdf7e782043cced84b002347acb11 zdeploy.cmd
|
||||||
d2ead96436507c8efedc3c72045e623137a1f844940c4ff57c9c7ebe67645511 zdeploy.ps1
|
c7c7ee1019808356bc4bad738edcd4e851ea797e28fe367a88fcd96bc5570770 zdeploy.ps1
|
||||||
30918a9260cb6220d6e2140edcf319fc0fa1e75b7fe36d0efe68b0d10cdde241 zec2.cmd
|
6fa05d7c47992801d0ad65095146764cc207b566dca85956b3152221ef9af368 zec2.cmd
|
||||||
695ec87ea6518933ee64524d369f1d0d5128bf6e54db31f555a888bdddcb9326 zec2.ps1
|
72217c04e8975f46b489bd7e223f9fda926d982a8e418fc2825c6aa4657f73b7 zec2.ps1
|
||||||
ce3209ed8eaab242286e76aeb11b1249239e2804e3de456a1609cc7caa780b43 zec2_rotatekeys.cmd
|
d0702f372ec5e47e2632229c61b71a9184edf0775d6e23af74e3f919d13eadb9 zec2_rotatekeys.cmd
|
||||||
f58277779b5c54814c29dda55ab567960e305fa9ef515b1206076be97f623093 zec2_rotatekeys.ps1
|
0dd4e6fda06dbb023884d051851bf4700c8db020b7ac319bb44c5974990839f7 zec2_rotatekeys.ps1
|
||||||
30ed73711eeddb518e02eb5e1968b7cc1dad51f27ecf42155b515b097487c3f3 zec2online.cmd
|
70b3833c4e9e3232f2b694768fb3c14434157cc9ec746f1dd39b3dedbfdb180e zec2online.cmd
|
||||||
1e8814928910ff4f3da59478511feb75280ba2e12111840fb47d2c5504741f0f zec2online.ps1
|
c3d5cca9cc94f51c97db01d22ceb140ef324e6f34c2fd1d4a3d0abbc0c3da263 zec2online.ps1
|
||||||
4ea81dc9e3cafc514b1e8224e633bf22bd88bfdc6e53545ff88a2a2d33715b4f ZHelpers.ps1
|
e806692a113bac17f364cbdf7167f4c86513819ee65733be997588355db57cdb ZHelpers.ps1
|
||||||
d97f9b5c68526c3295796b3042ee86194721eacab56b0efa0f3fdf16c881b0ea zkill.cmd
|
c62535257a4af8d7b08ef00a98c4d0ce907c91bf905bed478e4bef7eaea2c3a5 zkill.cmd
|
||||||
587de0b176788de917e713915ac468e44bf00cf9c9c7d6ea9b88aaec41ad6ea7 zkill.ps1
|
0b58265cab05f1fe86a4707b6c6c5c4c492192b1a3176951141399a44a5e26ca zkill.ps1
|
||||||
66556f55688d1f31e890d2b36143d3d0e2f5fcdded562e3d0a9591c02e827cda ZKiller.ps1
|
88db51d332c3439ccdf85aa4196fec1a6db4b5289eb18065ea7f881d37f625d4 ZKiller.ps1
|
||||||
2c9e0fa5dabb1544b6600cb60e5f66e89c787f217db246553b830152ab976ec3 ZKillOnly.ps1
|
32573d676ceabadee0d89b46de05b73deec5489a317771e13c4b15f7d34d8427 ZKillOnly.ps1
|
||||||
479d01a4c962eb1dba2dbfee913c3704048f29581a0a06d068c0fb9145c3a51c zmerge.ps1
|
392bddfa7e1c0fc21a828fa5af06d19156fd7305edfa2d5408f6335c3900716e zrelease.cmd
|
||||||
dd1cfde33aec53fc0df4a34e45704a59bf48e094fcafcc0ec49e9d13aa442b6f zpull.cmd
|
58b85d9a664d80d440be6b24332f3cec7d3fafb9021591e545a1ea916a17734a zrelease.ps1
|
||||||
9e746ad18b92ee7344061847b38c870f280421cf55d31e36a72a1514772e53a2 zpull.ps1
|
5ee7d7363362fab926487b8a622b9a0b7716ed08b2a74679e1d0e33659f58eec zrepair.cmd
|
||||||
4a57e270fc75ae5419bd27cf01b7dd6d8965be8dd58e0c80af985cb7bf27bdc5 zrelease.cmd
|
48b00d3443e001f7265d8cda85b384f5b5ce39bf5eda5bcf711492da147c455f zrepair.ps1
|
||||||
a6906e118f13442340cc4e0b14d7523d63e85d2bdb0eff8fd3a7c25567af8962 zrelease.ps1
|
4258b613e3913fbb24a54009452e829881a07a56956cd94b6878a1890d9cc422 zrestart.cmd
|
||||||
58e5b604cc260af7e644412094ff6e3bf799f80f9e52b9a83a044299f7704107 zrepair.cmd
|
0dab7d6a529d5d8cdd38c3b7e6236306735c36b4591b98664e45a5d1d178fb1b zrestart.ps1
|
||||||
186f7b0fb72808466ece328ea1a4e3a97bb2d4bae9f204ff7055e8b7f009614b zrepair.ps1
|
8d0680ec9f8b6431571ff590b552f469716da99e2e21c7cb5b92524afe15796f zrestartd.cmd
|
||||||
d97c9cf55b94c53dba49471d13f5c9870f2f50627ac5732ac4b60222da936244 zrestart.cmd
|
95c87c6320763b03f9b67792bc425e7b1b113cb0a2e4e7d4283911d821097d6a zsetup.cmd
|
||||||
32a8484d356c5f740d7a12f18bf0dd51a0c7da66010f6a6f553d976484315eef zrestart.ps1
|
7e6cc83ee10721dc23e4bb7c1ccbdc5f1fdc67e892d0af7cc98cb3695421c638 zsetup.ps1
|
||||||
2ebf37f72323cac3963a4a95d1ae414fecf4da1e1e618147663c6cf94d1ece24 zrestartd.cmd
|
05662f7de678a44f9f197414fbcbe43336b0aa499c63c1dac993b6c2b7d39f10 zsetup_mail.ps1
|
||||||
0a15ad0d341a5efdcd4c14e22e91d4b45f2bf87d0c5608fcf985200f6797bab4 zsetup.cmd
|
c4df12d8865b956b02d4f5d08edefd67a0ffc9036b23743fc262c64bc2681045 zstart.cmd
|
||||||
0d3042c5e44c3edf396005d0177f744c0c29536c25a50743e93a92156aa2a96c zsetup.ps1
|
d5a9068a3e931eb86204d45887c3c156219320871e7f4efb812729eec2a49b87 zstart.ps1
|
||||||
498ad7f8a7a56b332fede299e7172211ba360e76d9244c6c93f7cceae8a58619 zsetup_mail.ps1
|
b937fa0786eaccab596e5cdf67f40f0abadc1c52b3bef5e896e3cf16563d7b63 zstart_docker.cmd
|
||||||
46b3782d9b05649bdb1bcbd2a007bf6abc889abee420ac2f6e75530fa45a4694 zstart.cmd
|
380eda5611676c87b35e637f906ae6232e1fa53e8038e08d7c36f698807fa1ca zstart_docker.ps1
|
||||||
4b50cadf761f285f4fe655b143e04ec6b3565646ee8fb1ed165c234a201802b7 zstart.ps1
|
14a7d1916f5da8d36e458eb5eb653df915d20a80b0bb2e28b8e8d46256e0cb5d zstartd.cmd
|
||||||
2d74dcca3d4f51a28c84fbef5cc134126b61dfcc8ffb35e2d72f9596bf685809 zstart_docker.cmd
|
3d23878766284479939cf282479aa8506bad3f038a6af625a246e229cc2ef8f0 zstop.ps1
|
||||||
5016654d9ad68e11b85594be5e05318e19c5c6154174f734c43251970bc2086f zstart_docker.ps1
|
e4dc95d5c8749c22fa85c4885ab8d65e7bb37a543260709fca0cac3fa0380e62 zsync.cmd
|
||||||
1e1c5990e1dba41a165e797790f73cd33fcee5035eec65dd0d2610ea8b56a023 zstartd.cmd
|
0d8615e7d4078e5bbeee8b6373302a3eb138709863256e982b06a3882613123c zsync.ps1
|
||||||
a894876ad9fa1bfac6cacfec2a837914debb773e05b975d9b3cdf56589dd8ed2 zstop.ps1
|
32b5a359a6d3e3006879eac8ecca112990d07c5993e0feeec29de5666a555e4e zversion.cmd
|
||||||
2e165d35d9915099b98a14329c9eb2774b2bf2979c937bc3843d95b5b16400e7 zsync.cmd
|
327ca6074139fa696db8d1e4d2e4e01bf4bbbcb9145b8384de10fee14d564911 zversion.ps1
|
||||||
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.
|
||||||
-->
|
-->
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
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,7 +91,6 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,10 +101,9 @@ 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` 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).
|
- **`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).
|
||||||
- **`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`).
|
||||||
@ -136,8 +134,6 @@ 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
|
||||||
|
|
||||||
@ -382,21 +378,6 @@ 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -411,24 +392,6 @@ Verification walks its channels in trust order — docker-network `viaProxy`, th
|
|||||||
|
|
||||||
Each release is packaged as a zip in [`releases/`](releases/) — grab the latest `zscripts-v*.zip`, check it, unzip, done:
|
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
454
README.txt
@ -1,454 +0,0 @@
|
|||||||
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
68
SECURITY.md
@ -1,68 +0,0 @@
|
|||||||
# 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
73
SECURITY.txt
@ -1,73 +0,0 @@
|
|||||||
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.
|
||||||
-->
|
-->
|
||||||
|
|||||||
@ -1,296 +0,0 @@
|
|||||||
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.
|
|
||||||
478
ZHelpers.ps1
478
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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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
|
# Dockerfile's `COPY vendor ./vendor` fail at image build, which is a confusing
|
||||||
# confusing way to discover the archive filter ate a required build input.
|
# 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,9 +42,8 @@ $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, matching the bash port (zhelpers.sh does the
|
# ZCONFIG environment variable. Useful for pointing a run at an alternate
|
||||||
# same). Useful for pointing a run at an alternate config, and it is the seam
|
# config, and it is the seam the test suite uses to inject a fixture.
|
||||||
# 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")
|
||||||
@ -62,13 +61,13 @@ function Get-ZConfig {
|
|||||||
if (-not (Test-Path -LiteralPath $configPath)) {
|
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||||
Write-Host "ERROR: zconfig.json not found at $configPath" -ForegroundColor Red
|
Write-Host "ERROR: zconfig.json not found at $configPath" -ForegroundColor Red
|
||||||
Write-Host " Copy zconfig.example.json to zconfig.json and fill in your values." -ForegroundColor DarkGray
|
Write-Host " Copy zconfig.example.json to zconfig.json and fill in your values." -ForegroundColor DarkGray
|
||||||
Stop-ZTracking; exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$script:ZConfigCache = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
$script:ZConfigCache = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host "ERROR: Failed to parse zconfig.json - $($_.Exception.Message)" -ForegroundColor Red
|
Write-Host "ERROR: Failed to parse zconfig.json - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
Stop-ZTracking; exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
return $script:ZConfigCache
|
return $script:ZConfigCache
|
||||||
}
|
}
|
||||||
@ -88,7 +87,7 @@ function Get-ZProject {
|
|||||||
if (-not $proj) {
|
if (-not $proj) {
|
||||||
$available = (Get-ZProjectKeys) -join ', '
|
$available = (Get-ZProjectKeys) -join ', '
|
||||||
Write-Host "ERROR: Unknown project key '$Key'. Available: $available" -ForegroundColor Red
|
Write-Host "ERROR: Unknown project key '$Key'. Available: $available" -ForegroundColor Red
|
||||||
Stop-ZTracking; exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
return $proj
|
return $proj
|
||||||
}
|
}
|
||||||
@ -105,33 +104,6 @@ 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
|
||||||
@ -150,19 +122,18 @@ 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, matching the
|
# Options every deploy-path ssh/scp carries. Splat with @sshOpts.
|
||||||
# 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. `zdeploy umami` appeared to hang on "ensure
|
# particular step would be slow. One deploy appeared to hang on "ensure shared
|
||||||
# shared web network", a step whose entire body is `docker network create web
|
# 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 we would ever want to answer - the deploy key is
|
# There is no prompt here you would ever want to answer - a 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.
|
||||||
#
|
#
|
||||||
@ -176,16 +147,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. A deploy on 2026-08-19 stopped
|
# is dying, and this one was never established. One deploy stopped under
|
||||||
# under "ensure unzip installed", a step whose body short-circuits on an already
|
# "ensure unzip installed", a step whose body short-circuits when unzip is
|
||||||
# installed unzip; the server showed no ssh session at all (`who` empty, no
|
# already present; the server showed no ssh session at all (`who` empty, no
|
||||||
# docker build running), which is what a client-side stdin block looks like from
|
# docker build running), which is what a client-side stdin block looks like
|
||||||
# the other end. The zip had uploaded and prod stayed a PR behind.
|
# from the other end. The zip had uploaded and prod stayed a release 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, and the scripts that do pipe
|
# zdeploy passes only command strings. Any script that DOES pipe into ssh must
|
||||||
# (one-off registration and key-rotation scripts) build their own
|
# build its own option array - adding -n to those would break them, so do not
|
||||||
# option arrays. Adding -n to those would break them - do not hoist it.
|
# hoist this beyond the deploy path.
|
||||||
function Get-Ec2SshOpts {
|
function Get-Ec2SshOpts {
|
||||||
return @(
|
return @(
|
||||||
'-n',
|
'-n',
|
||||||
@ -199,9 +170,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 and the
|
# misread, because the caller's own error text is what the operator sees while
|
||||||
# usage text scrolls past above it - a deploy on 2026-08-19 reported "Likely
|
# the usage text scrolls past above it - one deploy reported "Likely server disk
|
||||||
# server disk space" while the box sat at 79% with 8.1 GB free.
|
# space" on a box with plenty of room.
|
||||||
#
|
#
|
||||||
# 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.
|
||||||
@ -246,11 +217,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 broke the day delete_branch_on_merge went on
|
# current branch, which breaks as soon as the remote deletes branches on merge:
|
||||||
# fleet-wide (2026-08-07): a repo still sitting on its just-merged PR branch
|
# a checkout still sitting on its just-merged PR branch pulls a ref the merge
|
||||||
# pulls a ref the merge deleted, and the deploy dies on "no such ref was
|
# deleted, and the deploy dies on "no such ref was fetched". Worse, when the ref
|
||||||
# fetched". Worse, when the ref DID still exist, pulling the feature branch
|
# DID still exist, pulling the feature branch meant a deploy could ship a
|
||||||
# meant a deploy could ship a branch, not main.
|
# branch rather than the default.
|
||||||
#
|
#
|
||||||
# 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
|
||||||
@ -304,11 +275,12 @@ function Invoke-DeployGitPull {
|
|||||||
# each deploy block the next one - the operator had to commit or
|
# each deploy block the next one - the operator had to commit or
|
||||||
# stash a change they never made. The guard exists to stop
|
# stash a change they never made. The guard exists to stop
|
||||||
# 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 PR, per the
|
||||||
# per the versioning rule - this only stops it being a gate.
|
# 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 '^.*/', ''
|
||||||
(Get-DeployStampFiles) -notcontains $path
|
$deployWritten -notcontains $path
|
||||||
}
|
}
|
||||||
if ($dirty) {
|
if ($dirty) {
|
||||||
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
$files = ($dirty | ForEach-Object { " $_" }) -join "`n"
|
||||||
@ -341,179 +313,6 @@ 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 {
|
||||||
@ -522,118 +321,15 @@ function Read-JsonBuildVersion {
|
|||||||
try { return Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
try { return Get-Content -LiteralPath $FilePath -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-ServerSideVersionCommand {
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Shell command that reads a project's live build ON the server, or
|
|
||||||
$null when the project has not configured one.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Four tools read a live build number, and all four did it by asking
|
|
||||||
the public edge: zdeploy's post-deploy check, zec2, zec2online, and
|
|
||||||
bash/zhelpers.sh. That works only for as long as the endpoint is
|
|
||||||
public, and it should not be: www's /build-version.json has been
|
|
||||||
blocked at the edge since an earlier security pass, and one app
|
|
||||||
answering /api/build-version to anyone is the inconsistency this
|
|
||||||
closes.
|
|
||||||
|
|
||||||
Going through the edge is also how a check reads the WRONG product.
|
|
||||||
The proxy answers from whichever vhost matches the Host header, so a
|
|
||||||
service with no public route gets somebody else's version back --
|
|
||||||
one project's deploy check compared another project's build against
|
|
||||||
its own and reported a failure on a deploy that had worked.
|
|
||||||
|
|
||||||
A project reached only on the shared docker network cannot be curled
|
|
||||||
from the host: an app container that publishes no port. It IS reachable by name
|
|
||||||
from another container on that network, which also exercises the real
|
|
||||||
HTTP path -- so this proves the app is serving, not merely that its
|
|
||||||
database knows a version.
|
|
||||||
|
|
||||||
Config, on the project's `verify` block:
|
|
||||||
|
|
||||||
"verify": {
|
|
||||||
"path": "/api/build-version",
|
|
||||||
"viaProxy": "evo_edge_proxy",
|
|
||||||
"upstream": "myapp_app:80"
|
|
||||||
}
|
|
||||||
|
|
||||||
Returns $null when either key is missing, so every project without
|
|
||||||
this config keeps exactly the behaviour it has today.
|
|
||||||
#>
|
|
||||||
param($Proj)
|
|
||||||
|
|
||||||
$v = $Proj.verify
|
|
||||||
if (-not $v) { return $null }
|
|
||||||
if (-not $v.viaProxy -or -not $v.upstream) { return $null }
|
|
||||||
$path = if ($v.path) { [string]$v.path } else { '/api/build-version' }
|
|
||||||
return "sudo docker exec $([string]$v.viaProxy) curl -s -m 8 http://$([string]$v.upstream)$path"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-LabelFromVersionJson {
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
The build label out of a version endpoint's JSON text, or $null.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Two field names in the fleet: one app answers `build_version` on
|
|
||||||
/api/build-version, another answers `version` on /health. Both mean
|
|
||||||
"the build that is live", so both are accepted rather than making an
|
|
||||||
app rename its own field.
|
|
||||||
#>
|
|
||||||
param([string]$Text)
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
|
|
||||||
try { $obj = $Text.Trim() | ConvertFrom-Json -ErrorAction Stop } catch { return $null }
|
|
||||||
if ($obj.build_version) { return [string]$obj.build_version }
|
|
||||||
if ($obj.version) { return [string]$obj.version }
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
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)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1024,29 +720,8 @@ function Add-ZTokensRecord {
|
|||||||
} catch { }
|
} catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Two blank lines below every z-script's output, so a run is visually separated
|
|
||||||
# from the next prompt instead of butting up against it. Emitted here because
|
|
||||||
# every script ends by calling Stop-ZTracking - including the usage and guard
|
|
||||||
# paths that `Stop-ZTracking; exit 1` - so one place covers every exit.
|
|
||||||
#
|
|
||||||
# -FinalNote prints one last line AFTER the tracking footer but BEFORE the blank
|
|
||||||
# lines, for a script that wants the bottom of the screen to say something more
|
|
||||||
# useful than a token count (zdeploy's "Last deployed at ...").
|
|
||||||
function Write-ZTrailer {
|
|
||||||
param([string]$FinalNote)
|
|
||||||
if ($FinalNote) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host $FinalNote -ForegroundColor Cyan
|
|
||||||
}
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host ""
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stop-ZTracking {
|
function Stop-ZTracking {
|
||||||
param([string]$FinalNote)
|
if (-not $global:_ZTrackPath) { return }
|
||||||
# The trailer is owed whether or not tracking ever started - a script that
|
|
||||||
# printed output still deserves the separation.
|
|
||||||
if (-not $global:_ZTrackPath) { Write-ZTrailer -FinalNote $FinalNote; return }
|
|
||||||
try { Stop-Transcript | Out-Null } catch {}
|
try { Stop-Transcript | Out-Null } catch {}
|
||||||
$tp = $global:_ZTrackPath
|
$tp = $global:_ZTrackPath
|
||||||
$global:_ZTrackPath = $null
|
$global:_ZTrackPath = $null
|
||||||
@ -1072,87 +747,4 @@ function Stop-ZTracking {
|
|||||||
Add-ZTokensRecord -Lines $lc -Chars $cc -Est $tok
|
Add-ZTokensRecord -Lines $lc -Chars $cc -Est $tok
|
||||||
} catch {}
|
} catch {}
|
||||||
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $tp -Force -ErrorAction SilentlyContinue
|
||||||
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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28"
|
"version": "v1.0.0.0.14"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
6d147836c97400ee29a76ed1250579a50ec1ba9d4f5773936e5f29571734e8e2 zscripts-v1.0.0.0.18.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
99b1c9da01dff32d21a77c26ae93535d0d0a9fd50e0b4ca8c2d2538e5c796256 zscripts-v1.0.0.0.19.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
e59dc1a0f6fd4d2af8cefd6698a60e2812b43ade74e6205cb0f2c75fdcaa925a zscripts-v1.0.0.0.20.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
fa9d023d2641044ff154d12047d87a31250741e0699fc5b1360ab359bd68b5f0 zscripts-v1.0.0.0.21.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
446428c605d06ee500f58145a7915f7b7ee7482dbf60c5499f64436aaf945e86 zscripts-v1.0.0.0.22.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
27d6c8f0ea632c0ca940c7900d8f4682066b5963847b9838a474cc5d4cbb758f zscripts-v1.0.0.0.23.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
250f68fe5fd46b95e3c393c19352aff1e7d2afc16cff2d76efb2487f1c8ba6df zscripts-v1.0.0.0.24.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
6f6f1bf1b46e014e0518ef4ffa2b64e455e894681d5b337ffdd947b0efd0d538 zscripts-v1.0.0.0.25.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
dfaa85f53e4dd16789ac0f1f8c9d7e506b56c0c068f4c5f8e2c4d1cce7db8d95 zscripts-v1.0.0.0.26.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
f5b940344ffd832032b0c62e65e77f94bc2de294e690c9fd68362deb99d21e82 zscripts-v1.0.0.0.27.zip
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
5eca5198ba500bb0e1ceb1e5000cfb405d5fb5024fa500daa8f64f9c012c1291 zscripts-v1.0.0.0.28.zip
|
|
||||||
@ -1,113 +0,0 @@
|
|||||||
# 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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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
|
||||||
#
|
#
|
||||||
@ -27,7 +27,6 @@ Write-Host ""
|
|||||||
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
||||||
Write-Host "ERROR: Script not found: $ScriptPath" -ForegroundColor Red
|
Write-Host "ERROR: Script not found: $ScriptPath" -ForegroundColor Red
|
||||||
Write-Host " Check paths.scriptsRoot in zconfig.json" -ForegroundColor DarkGray
|
Write-Host " Check paths.scriptsRoot in zconfig.json" -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +40,6 @@ if ($existingTask) {
|
|||||||
Write-Host "Deleted existing task." -ForegroundColor Green
|
Write-Host "Deleted existing task." -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Keeping existing task. Exiting." -ForegroundColor Yellow
|
Write-Host "Keeping existing task. Exiting." -ForegroundColor Yellow
|
||||||
Write-ZTrailer
|
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
108
site/index.html
108
site/index.html
@ -1,108 +0,0 @@
|
|||||||
<!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
BIN
site/og-card.png
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@ -1,7 +0,0 @@
|
|||||||
# 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,30 +73,14 @@ 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 { "$_" }
|
||||||
$result = [pscustomobject]@{
|
return [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.
|
||||||
|
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
# 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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
|||||||
@ -1,90 +0,0 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
|||||||
@ -1,73 +0,0 @@
|
|||||||
# 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,123 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
||||||
# Sanitization.Tests.ps1 — this repo is public and must stay standalone.
|
|
||||||
#
|
|
||||||
# WHY THIS EXISTS
|
|
||||||
# ---------------
|
|
||||||
# The toolkit is developed in a private checkout and copied here. Twice in three
|
|
||||||
# days a wholesale copy landed carrying environment-specific detail: real
|
|
||||||
# project names, internal hostnames, host disk figures, and references to
|
|
||||||
# scripts that exist only in the private copy. Each time it was caught by a
|
|
||||||
# human reading the diff, which is exactly the control that fails when a diff is
|
|
||||||
# 280 lines of good work with three bad words buried in it.
|
|
||||||
#
|
|
||||||
# So it is a test. A copy that reintroduces private detail turns the suite red
|
|
||||||
# at the moment it happens rather than at review.
|
|
||||||
#
|
|
||||||
# WHAT IT CANNOT DO
|
|
||||||
# -----------------
|
|
||||||
# This is a denylist, so it 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; the cost of a stale entry is zero.
|
|
||||||
|
|
||||||
BeforeAll {
|
|
||||||
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
|
||||||
|
|
||||||
# Scanned: everything a reader of the published repo can see. Skipped:
|
|
||||||
# .git (history is out of scope here), releases/ (published zips are
|
|
||||||
# immutable by policy - rewriting one would break its checksum), and this
|
|
||||||
# file, which necessarily contains every pattern it looks for.
|
|
||||||
$script:Scanned = @(
|
|
||||||
Get-ChildItem -LiteralPath $script:RepoRoot -Recurse -File |
|
|
||||||
Where-Object {
|
|
||||||
$_.Extension -in @('.ps1', '.cmd', '.md', '.txt', '.json', '.bats', '.sh') -and
|
|
||||||
$_.FullName -notlike '*\.git\*' -and
|
|
||||||
$_.FullName -notlike '*\releases\*' -and
|
|
||||||
$_.Name -ne 'Sanitization.Tests.ps1'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# The rules live in sanitization-patterns.psd1, not here, so the
|
|
||||||
# publisher in the private tree can read the SAME list. It used to keep
|
|
||||||
# its own, narrower one - secrets only, no identity rules - and therefore
|
|
||||||
# reported "clean" on files this suite rejects (evo.scripts#106).
|
|
||||||
$patternFile = Join-Path $PSScriptRoot 'sanitization-patterns.psd1'
|
|
||||||
if (-not (Test-Path -LiteralPath $patternFile)) {
|
|
||||||
throw "sanitization-patterns.psd1 is missing - the denylist has no source."
|
|
||||||
}
|
|
||||||
$script:Denied = (Import-PowerShellDataFile -LiteralPath $patternFile).Denied
|
|
||||||
if (-not $script:Denied -or $script:Denied.Count -eq 0) {
|
|
||||||
throw "sanitization-patterns.psd1 defined no rules - refusing to pass vacuously."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Hits {
|
|
||||||
param([string]$Pattern)
|
|
||||||
$hits = @()
|
|
||||||
foreach ($f in $script:Scanned) {
|
|
||||||
$m = Select-String -LiteralPath $f.FullName -Pattern $Pattern -AllMatches -ErrorAction SilentlyContinue
|
|
||||||
foreach ($line in $m) {
|
|
||||||
$rel = $line.Path.Substring($script:RepoRoot.Length).TrimStart('\')
|
|
||||||
$hits += "$rel`:$($line.LineNumber): $($line.Line.Trim())"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $hits
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe "public repo carries no private detail" {
|
|
||||||
|
|
||||||
It "finds files to scan at all" {
|
|
||||||
# Guards the guard: a bad filter here would make every test below pass
|
|
||||||
# vacuously, which is worse than no test.
|
|
||||||
$script:Scanned.Count | Should -BeGreaterThan 30
|
|
||||||
}
|
|
||||||
|
|
||||||
It "contains no <Name>" -ForEach @(
|
|
||||||
@{ Name = 'private project name' }
|
|
||||||
@{ Name = 'private product domain' }
|
|
||||||
@{ Name = 'private-only script' }
|
|
||||||
@{ Name = 'local drive path' }
|
|
||||||
@{ Name = 'operator home path' }
|
|
||||||
@{ Name = 'real pem key name' }
|
|
||||||
@{ Name = 'non-documentation IP' }
|
|
||||||
) {
|
|
||||||
$rule = $script:Denied | Where-Object { $_.Name -eq $Name }
|
|
||||||
$hits = Get-Hits -Pattern $rule.Pattern
|
|
||||||
$hits | Should -BeNullOrEmpty -Because "these look like private detail copied in from the internal toolkit:`n$($hits -join "`n")"
|
|
||||||
}
|
|
||||||
|
|
||||||
It "catches the current spelling <Sample>" -ForEach @(
|
|
||||||
@{ Rule = 'current product name'; Sample = 'evo.ehs answers build_version' }
|
|
||||||
@{ Rule = 'current product name'; Sample = 'evo-ai answers version on /health' }
|
|
||||||
@{ Rule = 'current product name'; Sample = 'upstream evoehs_app:80' }
|
|
||||||
@{ Rule = 'internal issue reference'; Sample = 'see evo.scripts#101 for the trap' }
|
|
||||||
) {
|
|
||||||
# The rename went past the old pattern: the dot and the hyphen break
|
|
||||||
# the word and the underscore hides the boundary, so a suite that ran
|
|
||||||
# green was trusted on a tree that named the fleet.
|
|
||||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq $Rule }).Pattern
|
|
||||||
$pattern | Should -Not -BeNullOrEmpty
|
|
||||||
($Sample -match $pattern) | Should -BeTrue
|
|
||||||
}
|
|
||||||
|
|
||||||
It "still allows this repo's own name" {
|
|
||||||
$pattern = ($script:Denied | Where-Object { $_.Name -eq 'current product name' }).Pattern
|
|
||||||
('https://github.com/evomedia-net/evo.zscripts' -match $pattern) | Should -BeFalse
|
|
||||||
}
|
|
||||||
|
|
||||||
It "still detects a planted violation" {
|
|
||||||
# Mutation check. Without this the suite passes just as happily when the
|
|
||||||
# patterns are broken as when the repo is clean - the failure mode that
|
|
||||||
# makes a denylist worthless.
|
|
||||||
$probe = Join-Path ([IO.Path]::GetTempPath()) ("sanitize-probe-" + [guid]::NewGuid().ToString("N") + ".ps1")
|
|
||||||
try {
|
|
||||||
Set-Content -LiteralPath $probe -Value '# deploy target /home/ubuntu/stack/example' -Encoding UTF8
|
|
||||||
$found = Select-String -LiteralPath $probe -Pattern ($script:Denied | Where-Object { $_.Name -eq 'operator home path' }).Pattern
|
|
||||||
$found | Should -Not -BeNullOrEmpty
|
|
||||||
}
|
|
||||||
finally { Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
# zstart's auto-pull must never stand between the user and a running server (#130).
|
|
||||||
#
|
|
||||||
# Invoke-Pester .\tests
|
|
||||||
#
|
|
||||||
# The report was "I merged it but not sure why it crashed, should have skipped
|
|
||||||
# it and moved on". It crashed on a pull that SUCCEEDED:
|
|
||||||
#
|
|
||||||
# git : From https://github.com/example/some-app
|
|
||||||
# At ZStart.ps1:206 char:24
|
|
||||||
# + $pullOut = git pull --ff-only 2>&1
|
|
||||||
#
|
|
||||||
# Under $ErrorActionPreference = 'Stop', a stderr redirect on a native command
|
|
||||||
# in Windows PowerShell 5.1 wraps every stderr line in a terminating
|
|
||||||
# ErrorRecord - and git writes ordinary fetch progress ("From ...") to stderr.
|
|
||||||
# So the try block died before its own "Auto-pull skipped" branch could run.
|
|
||||||
#
|
|
||||||
# These tests drive REAL git under 'Stop' on the same host the defect lives
|
|
||||||
# on. A stand-in that faked git's output would prove nothing about the stream
|
|
||||||
# semantics that are the entire bug; one test asserts the fixture really does
|
|
||||||
# put that "From ..." line on stderr, so the reproduction cannot quietly go
|
|
||||||
# stale the way an LF changelog fixture once did.
|
|
||||||
#
|
|
||||||
# ZHelpers.ps1 is dot-sourced rather than ZStart.ps1, which runs its main flow
|
|
||||||
# on load. That is also why the logic moved into a helper: it was untestable
|
|
||||||
# where it sat.
|
|
||||||
|
|
||||||
BeforeAll {
|
|
||||||
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
|
|
||||||
|
|
||||||
$script:tmpRoots = New-Object System.Collections.ArrayList
|
|
||||||
|
|
||||||
function New-TempDir {
|
|
||||||
param([string]$Tag)
|
|
||||||
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zstart-pull-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
|
|
||||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
|
||||||
[void]$script:tmpRoots.Add($dir)
|
|
||||||
return $dir
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-Git {
|
|
||||||
# Test plumbing only. Runs git quietly from a directory and fails the
|
|
||||||
# test loudly if it did not work - the thing under test does its own
|
|
||||||
# git handling and must not go through here.
|
|
||||||
param([string]$In, [string[]]$GitArgs)
|
|
||||||
Push-Location -LiteralPath $In
|
|
||||||
try {
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
# Quote anything with whitespace: a Windows temp root usually
|
|
||||||
# sits under a user profile whose name has a space in it, and
|
|
||||||
# an unquoted path splits into two arguments on the way through
|
|
||||||
# cmd.
|
|
||||||
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
|
|
||||||
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
|
|
||||||
return $out
|
|
||||||
} finally { Pop-Location }
|
|
||||||
}
|
|
||||||
|
|
||||||
function New-ClonePair {
|
|
||||||
# A bare "origin" and a working clone tracking main, one commit in.
|
|
||||||
# Returns @{ Bare; Clone } and leaves a helper to advance origin.
|
|
||||||
$bare = New-TempDir 'origin'
|
|
||||||
Invoke-Git $bare @('init', '--bare', '--initial-branch=main', '--quiet') | Out-Null
|
|
||||||
$seed = New-TempDir 'seed'
|
|
||||||
Invoke-Git $seed @('init', '--initial-branch=main', '--quiet') | Out-Null
|
|
||||||
Invoke-Git $seed @('config', 'user.email', 'test@example.invalid') | Out-Null
|
|
||||||
Invoke-Git $seed @('config', 'user.name', 'zstart test') | Out-Null
|
|
||||||
Set-Content -LiteralPath (Join-Path $seed 'a.txt') -Value 'one'
|
|
||||||
Invoke-Git $seed @('add', '.') | Out-Null
|
|
||||||
Invoke-Git $seed @('commit', '-q', '-m', 'one') | Out-Null
|
|
||||||
Invoke-Git $seed @('remote', 'add', 'origin', $bare) | Out-Null
|
|
||||||
Invoke-Git $seed @('push', '-q', '-u', 'origin', 'main') | Out-Null
|
|
||||||
|
|
||||||
$clone = New-TempDir 'clone'
|
|
||||||
Invoke-Git (Split-Path -Parent $clone) @('clone', '-q', $bare, $clone) | Out-Null
|
|
||||||
Invoke-Git $clone @('config', 'user.email', 'test@example.invalid') | Out-Null
|
|
||||||
Invoke-Git $clone @('config', 'user.name', 'zstart test') | Out-Null
|
|
||||||
return [pscustomobject]@{ Bare = $bare; Clone = $clone; Seed = $seed }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Add-OriginCommit {
|
|
||||||
# Advance origin from the seed checkout, so the clone has something
|
|
||||||
# to fetch - which is exactly what makes git print "From ..." on
|
|
||||||
# stderr.
|
|
||||||
param($Pair, [string]$Name = 'two')
|
|
||||||
Set-Content -LiteralPath (Join-Path $Pair.Seed "$Name.txt") -Value $Name
|
|
||||||
Invoke-Git $Pair.Seed @('add', '.') | Out-Null
|
|
||||||
Invoke-Git $Pair.Seed @('commit', '-q', '-m', $Name) | Out-Null
|
|
||||||
Invoke-Git $Pair.Seed @('push', '-q', 'origin', 'main') | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Head {
|
|
||||||
param([string]$Repo)
|
|
||||||
return (Invoke-Git $Repo @('rev-parse', 'HEAD') | Select-Object -Last 1).ToString().Trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AfterAll {
|
|
||||||
foreach ($d in $script:tmpRoots) {
|
|
||||||
try { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } catch { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe "Invoke-StartGitPull" {
|
|
||||||
|
|
||||||
Context "the reported case: origin has new commits" {
|
|
||||||
|
|
||||||
BeforeAll {
|
|
||||||
$script:pair = New-ClonePair
|
|
||||||
Add-OriginCommit $script:pair
|
|
||||||
$script:originTip = Get-Head $script:pair.Seed
|
|
||||||
}
|
|
||||||
|
|
||||||
It "the fixture really puts fetch progress on stderr - the shape of the bug" {
|
|
||||||
# Checked on a second clone so the one under test is still behind.
|
|
||||||
$probe = New-TempDir 'probe'
|
|
||||||
Invoke-Git (Split-Path -Parent $probe) @('clone', '-q', $script:pair.Bare, $probe) | Out-Null
|
|
||||||
Add-OriginCommit $script:pair 'three'
|
|
||||||
Push-Location -LiteralPath $probe
|
|
||||||
try {
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
# stderr only: stdout is dropped, so anything captured came
|
|
||||||
# from the stream that ErrorRecords are made from.
|
|
||||||
$stderr = & cmd /c "git fetch origin 2>&1 1>nul"
|
|
||||||
} finally { Pop-Location }
|
|
||||||
($stderr -join "`n") | Should -Match '(?m)^From '
|
|
||||||
}
|
|
||||||
|
|
||||||
It "does not abort under ErrorActionPreference = 'Stop'" {
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
{ $script:r = Invoke-StartGitPull -Root $script:pair.Clone } | Should -Not -Throw
|
|
||||||
}
|
|
||||||
|
|
||||||
It "reports success" {
|
|
||||||
$script:r.Ok | Should -BeTrue
|
|
||||||
$script:r.Skipped | Should -BeFalse
|
|
||||||
$script:r.Message | Should -Match '^Now at: '
|
|
||||||
}
|
|
||||||
|
|
||||||
It "actually fast-forwarded the checkout" {
|
|
||||||
Get-Head $script:pair.Clone | Should -Be (Get-Head $script:pair.Seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
It "leaves the caller's ErrorActionPreference as it found it" {
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
Invoke-StartGitPull -Root $script:pair.Clone | Out-Null
|
|
||||||
$ErrorActionPreference | Should -Be 'Stop'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Context "nothing to fetch" {
|
|
||||||
|
|
||||||
It "is Ok and says so, not a skip" {
|
|
||||||
$pair = New-ClonePair
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$r = Invoke-StartGitPull -Root $pair.Clone
|
|
||||||
$r.Ok | Should -BeTrue
|
|
||||||
$r.Message | Should -Be 'Already up to date.'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Context "a pull that cannot complete" {
|
|
||||||
|
|
||||||
It "diverged history is reported, not thrown, and the checkout is left alone" {
|
|
||||||
$pair = New-ClonePair
|
|
||||||
# Local commit on the clone AND a different one on origin.
|
|
||||||
Set-Content -LiteralPath (Join-Path $pair.Clone 'local.txt') -Value 'mine'
|
|
||||||
Invoke-Git $pair.Clone @('add', '.') | Out-Null
|
|
||||||
Invoke-Git $pair.Clone @('commit', '-q', '-m', 'local') | Out-Null
|
|
||||||
$localTip = Get-Head $pair.Clone
|
|
||||||
Add-OriginCommit $pair 'theirs'
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
{ $script:div = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
|
||||||
$script:div.Ok | Should -BeFalse
|
|
||||||
$script:div.Skipped | Should -BeFalse
|
|
||||||
$script:div.Message | Should -Match 'cannot fast-forward'
|
|
||||||
Get-Head $pair.Clone | Should -Be $localTip
|
|
||||||
}
|
|
||||||
|
|
||||||
It "a branch with no upstream is skipped - and never switched away from" {
|
|
||||||
$pair = New-ClonePair
|
|
||||||
Invoke-Git $pair.Clone @('checkout', '-q', '-b', 'feature/thing') | Out-Null
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
{ $script:noup = Invoke-StartGitPull -Root $pair.Clone } | Should -Not -Throw
|
|
||||||
$script:noup.Skipped | Should -BeTrue
|
|
||||||
$script:noup.Message | Should -Match "no upstream"
|
|
||||||
(Invoke-Git $pair.Clone @('rev-parse', '--abbrev-ref', 'HEAD') | Select-Object -Last 1).ToString().Trim() |
|
|
||||||
Should -Be 'feature/thing'
|
|
||||||
}
|
|
||||||
|
|
||||||
It "a directory that is not a repo is skipped, not thrown" {
|
|
||||||
$dir = New-TempDir 'norepo'
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
{ $script:norepo = Invoke-StartGitPull -Root $dir } | Should -Not -Throw
|
|
||||||
$script:norepo.Skipped | Should -BeTrue
|
|
||||||
$script:norepo.Message | Should -Match 'not a git repo'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Context "housekeeping" {
|
|
||||||
|
|
||||||
It "restores GIT_TERMINAL_PROMPT to whatever it was" {
|
|
||||||
$pair = New-ClonePair
|
|
||||||
$prev = $env:GIT_TERMINAL_PROMPT
|
|
||||||
try {
|
|
||||||
$env:GIT_TERMINAL_PROMPT = 'sentinel'
|
|
||||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
|
||||||
$env:GIT_TERMINAL_PROMPT | Should -Be 'sentinel'
|
|
||||||
} finally { $env:GIT_TERMINAL_PROMPT = $prev }
|
|
||||||
}
|
|
||||||
|
|
||||||
It "returns to the directory it was called from" {
|
|
||||||
$pair = New-ClonePair
|
|
||||||
$here = (Get-Location).Path
|
|
||||||
Invoke-StartGitPull -Root $pair.Clone | Out-Null
|
|
||||||
(Get-Location).Path | Should -Be $here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Describe "ZStart.ps1 uses the helper" {
|
|
||||||
|
|
||||||
BeforeAll {
|
|
||||||
$script:zstart = Get-Content -Raw (Join-Path (Split-Path -Parent $PSScriptRoot) "ZStart.ps1")
|
|
||||||
}
|
|
||||||
|
|
||||||
It "no longer carries its own redirected pull - the line that crashed" {
|
|
||||||
$script:zstart | Should -Not -Match 'git pull --ff-only 2>&1'
|
|
||||||
}
|
|
||||||
|
|
||||||
It "calls Invoke-StartGitPull" {
|
|
||||||
$script:zstart | Should -Match 'Invoke-StartGitPull -Root'
|
|
||||||
}
|
|
||||||
|
|
||||||
It "still tells the user when it skipped, and how to stop it trying" {
|
|
||||||
$script:zstart | Should -Match 'Auto-pull skipped'
|
|
||||||
$script:zstart | Should -Match 'start\.gitPull=false'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
|
|||||||
@ -1,58 +0,0 @@
|
|||||||
<#
|
|
||||||
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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.
|
||||||
#
|
#
|
||||||
@ -170,4 +170,4 @@ Write-Host ""
|
|||||||
Write-Host "Note: zdeploy output varies significantly between runs." -ForegroundColor DarkGray
|
Write-Host "Note: zdeploy output varies significantly between runs." -ForegroundColor DarkGray
|
||||||
Write-Host " First run after package updates can be 2-3x larger than a cached run." -ForegroundColor DarkGray
|
Write-Host " First run after package updates can be 2-3x larger than a cached run." -ForegroundColor DarkGray
|
||||||
Write-Host " Run zdeploy twice and use the second (cached) figure for TOKEN_SAVINGS.md." -ForegroundColor DarkGray
|
Write-Host " Run zdeploy twice and use the second (cached) figure for TOKEN_SAVINGS.md." -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
|
|||||||
@ -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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
# zchecksums.ps1 - verify (or regenerate) SHA-256 checksums for the scripts.
|
||||||
#
|
#
|
||||||
@ -54,13 +54,6 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
# Two blank lines after this script's output, matching every other z-script, so
|
|
||||||
# a run is visually separated from the next prompt. Local rather than from
|
|
||||||
# ZHelpers: this script is deliberately standalone, so it can run from an
|
|
||||||
# extracted release zip with nothing beside it.
|
|
||||||
function Write-ZTrailer { Write-Host ""; Write-Host "" }
|
|
||||||
|
|
||||||
|
|
||||||
$ManifestName = "CHECKSUMS.txt"
|
$ManifestName = "CHECKSUMS.txt"
|
||||||
$Manifest = Join-Path $PSScriptRoot $ManifestName
|
$Manifest = Join-Path $PSScriptRoot $ManifestName
|
||||||
|
|
||||||
@ -106,14 +99,13 @@ if ($Update) {
|
|||||||
Write-Host "=== zchecksums (updated) ===" -ForegroundColor Cyan
|
Write-Host "=== zchecksums (updated) ===" -ForegroundColor Cyan
|
||||||
Write-Host (" Wrote {0} with {1} entries." -f $ManifestName, $files.Count) -ForegroundColor Green
|
Write-Host (" Wrote {0} with {1} entries." -f $ManifestName, $files.Count) -ForegroundColor Green
|
||||||
Write-Host " Commit it alongside the script change, or verification will fail." -ForegroundColor DarkGray
|
Write-Host " Commit it alongside the script change, or verification will fail." -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
$expected = Read-Manifest
|
$expected = Read-Manifest
|
||||||
if ($null -eq $expected) {
|
if ($null -eq $expected) {
|
||||||
Write-Host "ERROR: $ManifestName not found. Run 'zchecksums -Update' to create it." -ForegroundColor Red
|
Write-Host "ERROR: $ManifestName not found. Run 'zchecksums -Update' to create it." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,11 +134,11 @@ if (-not $Quiet) {
|
|||||||
|
|
||||||
if ($bad -eq 0) {
|
if ($bad -eq 0) {
|
||||||
Write-Host (" OK - {0} file(s) match {1}." -f $ok, $ManifestName) -ForegroundColor Green
|
Write-Host (" OK - {0} file(s) match {1}." -f $ok, $ManifestName) -ForegroundColor Green
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host (" FAILED - {0} ok, {1} changed, {2} missing, {3} unlisted." -f $ok, $changed.Count, $missing.Count, $unlisted.Count) -ForegroundColor Red
|
Write-Host (" FAILED - {0} ok, {1} changed, {2} missing, {3} unlisted." -f $ok, $changed.Count, $missing.Count, $unlisted.Count) -ForegroundColor Red
|
||||||
Write-Host " If you changed a script on purpose, run: zchecksums -Update" -ForegroundColor DarkGray
|
Write-Host " If you changed a script on purpose, run: zchecksums -Update" -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Copy this file to zconfig.json and fill in your values. zconfig.json is gitignored \u2014 never commit it.",
|
"_comment": "Copy this file to zconfig.json and fill in your values. zconfig.json is gitignored — never commit it.",
|
||||||
|
|
||||||
"ec2": {
|
"ec2": {
|
||||||
"ip": "YOUR_SERVER_IP",
|
"ip": "YOUR_SERVER_IP",
|
||||||
"user": "YOUR_SSH_USER",
|
"user": "YOUR_SSH_USER",
|
||||||
"pemKey": "C:\\Users\\YourUser\\.ssh\\YourKey.pem",
|
"pemKey": "C:\\Users\\YourUser\\.ssh\\YourKey.pem",
|
||||||
"stackRoot": "/home/YOUR_SSH_USER/stack"
|
"stackRoot": "/home/YOUR_SSH_USER/stack"
|
||||||
},
|
},
|
||||||
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"temp": "C:\\YourRoot\\temp",
|
"temp": "C:\\YourRoot\\temp",
|
||||||
"backupsLocal": "C:\\YourRoot\\backups\\projects",
|
"backupsLocal": "C:\\YourRoot\\backups\\projects",
|
||||||
@ -13,8 +15,10 @@
|
|||||||
"scriptsRoot": "C:\\YourRoot\\zscripts",
|
"scriptsRoot": "C:\\YourRoot\\zscripts",
|
||||||
"oneDriveBackups": ""
|
"oneDriveBackups": ""
|
||||||
},
|
},
|
||||||
|
|
||||||
"projects": {
|
"projects": {
|
||||||
"_comment": "Rename these keys to your own project names \u2014 the key IS the command argument: zstart pyapp, zdeploy viteapp, zbackup nextapp. Add as many projects as you like. Keys starting with _ are ignored.",
|
"_comment": "Rename these keys to your own project names — the key IS the command argument: zstart pyapp, zdeploy viteapp, zbackup nextapp. Add as many projects as you like. Keys starting with _ are ignored.",
|
||||||
|
|
||||||
"pyapp": {
|
"pyapp": {
|
||||||
"label": "My Python App",
|
"label": "My Python App",
|
||||||
"kind": "python",
|
"kind": "python",
|
||||||
@ -22,93 +26,68 @@
|
|||||||
"startModule": "pyapp.main",
|
"startModule": "pyapp.main",
|
||||||
"_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port <dev> --reload).",
|
"_startApp_note": "ASGI/FastAPI app? Use \"startApp\": \"app.main:app\" instead of startModule (runs uvicorn --port <dev> --reload).",
|
||||||
"install": "-e .",
|
"install": "-e .",
|
||||||
"ports": {
|
"ports": { "dev": 8080 },
|
||||||
"dev": 8080
|
|
||||||
},
|
|
||||||
"domain": "pyapp.yourdomain.com",
|
"domain": "pyapp.yourdomain.com",
|
||||||
"start": {
|
"start": {
|
||||||
"_comment": "Optional zstart pre-steps: gitPull runs 'git pull --ff-only' first; env sets variables for the server process.",
|
"_comment": "Optional zstart pre-steps: gitPull runs 'git pull --ff-only' first; env sets variables for the server process.",
|
||||||
"gitPull": true,
|
"gitPull": true,
|
||||||
"env": {
|
"env": { "MYAPP_DEBUG": "1" }
|
||||||
"MYAPP_DEBUG": "1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"db": {
|
|
||||||
"user": "pyapp_user",
|
|
||||||
"name": "pyapp_db"
|
|
||||||
},
|
},
|
||||||
|
"db": { "user": "pyapp_user", "name": "pyapp_db" },
|
||||||
"remote": {
|
"remote": {
|
||||||
"path": "/home/YOUR_SSH_USER/stack/pyapp",
|
"path": "/home/YOUR_SSH_USER/stack/pyapp",
|
||||||
"composeDir": "/home/YOUR_SSH_USER/stack/pyapp/docker",
|
"composeDir": "/home/YOUR_SSH_USER/stack/pyapp/docker",
|
||||||
"appService": "app"
|
"appService": "app"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"_comment": "Optional post-deploy build check. Two ways to reach the app, both ON the server rather than through the public proxy - which answers from whichever vhost matches the Host header, so a container with no public route gets another site's version back. Use port+expect when the app publishes a host port; use viaProxy+upstream when it does not, or when its version endpoint is deliberately not public.",
|
"_comment": "Optional: after deploy, curl this ON the server (localhost:port+path) and require the substring. The accurate check for apps not published through the edge proxy.",
|
||||||
"port": 8080,
|
"port": 8080,
|
||||||
"path": "/health",
|
"path": "/health",
|
||||||
"expect": "\"status\":\"ok\"",
|
"expect": "\"status\":\"ok\""
|
||||||
"viaProxy": "edge_proxy_container",
|
|
||||||
"upstream": "app_container:80",
|
|
||||||
"_viaProxy_note": "Runs: docker exec <viaProxy> curl -s http://<upstream><path>. Reads the build from inside the shared docker network, and exercises the real HTTP path - so it proves the app is serving, not just that its database knows a version. Omit both keys to keep the previous behaviour."
|
|
||||||
},
|
},
|
||||||
"deploy": {
|
"deploy": {
|
||||||
"zipName": "PyAppDeploy.zip",
|
"zipName": "PyAppDeploy.zip",
|
||||||
"gitPull": true,
|
"gitPull": true,
|
||||||
"tagOnDeploy": false,
|
"exclude": ["docs"],
|
||||||
"exclude": [
|
|
||||||
"docs"
|
|
||||||
],
|
|
||||||
"_comment": "preserve: server-side files/dirs in the project dir that deploys must never delete (.env* files are always preserved)",
|
"_comment": "preserve: server-side files/dirs in the project dir that deploys must never delete (.env* files are always preserved)",
|
||||||
"preserve": [
|
"preserve": ["keys", "seed-data"]
|
||||||
"keys",
|
|
||||||
"seed-data"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"viteapp": {
|
"viteapp": {
|
||||||
"label": "My Vite Site",
|
"label": "My Vite Site",
|
||||||
"kind": "vite",
|
"kind": "vite",
|
||||||
"localRoot": "C:\\YourRoot\\viteapp",
|
"localRoot": "C:\\YourRoot\\viteapp",
|
||||||
"ports": {
|
"ports": { "dev": 5173 },
|
||||||
"dev": 5173
|
|
||||||
},
|
|
||||||
"domain": "www.yourdomain.com",
|
"domain": "www.yourdomain.com",
|
||||||
"remote": {
|
"remote": {
|
||||||
"path": "/home/YOUR_SSH_USER/stack/viteapp",
|
"path": "/home/YOUR_SSH_USER/stack/viteapp",
|
||||||
"containerName": "viteapp"
|
"containerName": "viteapp"
|
||||||
},
|
},
|
||||||
"deploy": {
|
"deploy": { "zipName": "ViteAppDeploy.zip" }
|
||||||
"zipName": "ViteAppDeploy.zip"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"nextapp": {
|
"nextapp": {
|
||||||
"label": "My Next.js App",
|
"label": "My Next.js App",
|
||||||
"kind": "nextjs",
|
"kind": "nextjs",
|
||||||
"localRoot": "C:\\YourRoot\\nextapp",
|
"localRoot": "C:\\YourRoot\\nextapp",
|
||||||
"ports": {
|
"ports": { "dev": 4173, "prod": 3000 },
|
||||||
"dev": 4173,
|
|
||||||
"prod": 3000
|
|
||||||
},
|
|
||||||
"domain": "app.yourdomain.com",
|
"domain": "app.yourdomain.com",
|
||||||
"db": {
|
"db": { "user": "nextapp_user", "name": "nextapp_db" },
|
||||||
"user": "nextapp_user",
|
|
||||||
"name": "nextapp_db"
|
|
||||||
},
|
|
||||||
"migrations": "prisma",
|
"migrations": "prisma",
|
||||||
"remote": {
|
"remote": {
|
||||||
"path": "/home/YOUR_SSH_USER/stack/nextapp",
|
"path": "/home/YOUR_SSH_USER/stack/nextapp",
|
||||||
"appService": "web"
|
"appService": "web"
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"_comment": "Optional: after deploy, curl this ON the server (localhost:port+path). Add it when compose publishes the app to 127.0.0.1 only, as it usually does behind the edge proxy \u2014 probing the public IP on that port can never answer. Redirects are followed, so a Next.js '/' that 307s to '/login' still verifies. Takes precedence over the domain check.",
|
"_comment": "Optional: after deploy, curl this ON the server (localhost:port+path). Add it when compose publishes the app to 127.0.0.1 only, as it usually does behind the edge proxy — probing the public IP on that port can never answer. Redirects are followed, so a Next.js '/' that 307s to '/login' still verifies. Takes precedence over the domain check.",
|
||||||
"port": 3000,
|
"port": 3000,
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expect": ""
|
"expect": ""
|
||||||
},
|
},
|
||||||
"deploy": {
|
"deploy": { "zipName": "NextAppDeploy.zip" }
|
||||||
"zipName": "NextAppDeploy.zip"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"edge": {
|
"edge": {
|
||||||
"label": "Edge Nginx Proxy",
|
"label": "Edge Nginx Proxy",
|
||||||
"kind": "edge",
|
"kind": "edge",
|
||||||
@ -119,10 +98,10 @@
|
|||||||
"path": "/home/YOUR_SSH_USER/stack/edge"
|
"path": "/home/YOUR_SSH_USER/stack/edge"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zdeploy.ps1" %*
|
||||||
|
|||||||
637
zdeploy.ps1
637
zdeploy.ps1
@ -1,17 +1,14 @@
|
|||||||
# 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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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
|
||||||
# project's "kind": python | vite | nextjs | edge | docker | static.
|
# project's "kind": python | vite | nextjs | edge | docker.
|
||||||
#
|
#
|
||||||
# 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)
|
||||||
#
|
#
|
||||||
@ -21,11 +18,12 @@
|
|||||||
# 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 a pseudo-project, not a zconfig entry: it runs `ztokens -Publish`
|
# "ztokens" is an OPTIONAL pseudo-project, not a zconfig entry: it runs
|
||||||
# from the sibling ztokens repo, refreshing the token-stats.json the public
|
# `ztokens -Publish` from a sibling ztokens checkout, if you have one, to
|
||||||
# zscripts page charts. `all` runs it first automatically; called standalone,
|
# refresh a token-stats.json a site can chart. With no such checkout the step
|
||||||
# list it before a site project (as above) so that project's deploy zip picks
|
# prints a skip and the rest of the run is unaffected. `all` runs it first
|
||||||
# up the freshly written file.
|
# automatically; called standalone, list it before a site project (as above) so
|
||||||
|
# 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
|
||||||
@ -46,13 +44,7 @@
|
|||||||
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"
|
||||||
@ -74,170 +66,13 @@ 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 " -Scan / -s reports which projects have merged work that is not live, then offers to deploy just those." -ForegroundColor Gray
|
Write-Host " 'ztokens' refreshes live-usage stats, if a sibling ztokens checkout exists." -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -256,80 +91,6 @@ 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' })
|
||||||
@ -341,34 +102,6 @@ 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 }
|
||||||
@ -399,12 +132,10 @@ 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 '; '
|
||||||
# Also through the wrapper (#119): the out-of-space branch above writes its
|
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $preflightCmd
|
||||||
# ERROR to stderr and exits 11, so a bare ssh would surface a
|
if ($LASTEXITCODE -ne 0) {
|
||||||
# NativeCommandError instead of the actionable message below - exactly when
|
throw "Server pre-flight cleanup failed (exit $LASTEXITCODE). Root volume too full (need ~1.5 GB free, ideally 3+)."
|
||||||
# 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.
|
||||||
@ -439,96 +170,30 @@ function Invoke-RemoteUnzip {
|
|||||||
Invoke-Ec2Step "unzip $ZipName" $bash
|
Invoke-Ec2Step "unzip $ZipName" $bash
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Operator-file preservation (issue #2, hardened in #107) ──────────────────
|
# ── Operator-file preservation (issue #2) ────────────────────────────────────
|
||||||
# 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 = Get-PreserveTarball -Key $Key
|
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
||||||
$list = $tarball -replace '\.tgz$', '.list'
|
# NOTE: no embedded quotes or $( ) here - PowerShell 5.1 strips embedded
|
||||||
$glob = "$RemoteHome/preserve_${Key}_*"
|
# double quotes when passing args to ssh.exe, silently corrupting the
|
||||||
$drop = $script:PreserveKeep + 1
|
# remote command. Globs expand remotely; tar archives whatever exists
|
||||||
|
# and its nonzero exit for missing paths is deliberately swallowed.
|
||||||
# NOTE: no embedded double quotes or $( ) here - PowerShell 5.1 strips
|
Invoke-Ec2Step "preserve operator files ($spec)" "rm -f $tarball; cd $RemotePath && tar -czf $tarball $spec 2>/dev/null; true"
|
||||||
# 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 = Get-PreserveTarball -Key $Key
|
$tarball = "$RemoteHome/preserve_${Key}.tgz"
|
||||||
# Overwrites, deliberately: server-side operator files beat whatever the
|
Invoke-Ec2Step "restore operator files" "test -f $tarball && tar -xzf $tarball -C $RemotePath; rm -f $tarball; true"
|
||||||
# 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
|
||||||
@ -543,19 +208,9 @@ function Wait-VerifyStaticBuild {
|
|||||||
# Expect the COMMITTED stamp, not +1: builds no longer self-bump (a
|
# Expect the COMMITTED stamp, not +1: builds no longer self-bump (a
|
||||||
# prebuild hook incremented inside the image, so the served version
|
# prebuild hook incremented inside the image, so the served version
|
||||||
# matched no commit and every build dirtied the tree). The counter now
|
# matched no commit and every build dirtied the tree). The counter now
|
||||||
# advances deliberately — one version bump per release — so "is the
|
# advances deliberately — one version bump per merged PR — 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)
|
||||||
@ -575,7 +230,7 @@ function Wait-VerifyStaticBuild {
|
|||||||
}
|
}
|
||||||
if ($r) {
|
if ($r) {
|
||||||
$remoteLabel = Get-LabelFromBuildJsonObj $r
|
$remoteLabel = Get-LabelFromBuildJsonObj $r
|
||||||
if ($remoteLabel -and $remoteLabel -eq $expectedLabel) {
|
if ($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
|
||||||
}
|
}
|
||||||
@ -592,87 +247,32 @@ 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
|
||||||
# deploy.verifyHost (or domain) decides which edge vhost may be asked;
|
$headers = @{}
|
||||||
# both are consumed inside Get-VerifyAttempts now, where "no host at all"
|
# deploy.verifyHost overrides domain for verification only. The Host header
|
||||||
# excludes the edge channel entirely rather than defaulting to whatever
|
# decides which edge vhost answers, and a project's public host can be
|
||||||
# vhost the proxy serves (#101). verifyHost exists for a domain retired
|
# deliberately unroutable while the app is perfectly healthy - that is why
|
||||||
# ahead of its replacement - a takedown once had a project's public host
|
# the override exists. Reach for it when a domain is being retired ahead of
|
||||||
# answering 410 while the app was healthy; no project sets it today.
|
# its replacement: the old host may be returning 410 while the new one has
|
||||||
# Every retry walks the channels in trust order - docker-network exec,
|
# no DNS yet, so neither answers even though the app is fine.
|
||||||
# then localhost port, then (only with a Host to route by) the edge.
|
$verifyHost = if ($Proj.deploy -and $Proj.deploy.verifyHost) { $Proj.deploy.verifyHost } else { $Proj.domain }
|
||||||
# The choice used to be made ONCE, before the loop, by probing each
|
if ($verifyHost) { $headers['Host'] = $verifyHost }
|
||||||
# channel - but the probes ran at the exact moment step [5] had
|
|
||||||
# restarted the app, so both good channels were briefly down and the
|
|
||||||
# whole window was spent on the edge. For a project with no domain that
|
|
||||||
# meant the default vhost: one project's check read a DIFFERENT
|
|
||||||
# project's build number, twice in a single day (#101). Re-resolving per
|
|
||||||
# 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
|
|
||||||
$attempts = Get-VerifyAttempts -Proj $Proj -ExecCmd $execCmd
|
|
||||||
$TimeoutSec = Get-VerifyTimeout -Proj $Proj -DefaultSec $TimeoutSec
|
|
||||||
if ($attempts.Count -eq 0) {
|
|
||||||
# No trustworthy channel exists: no viaProxy, no port, no host to
|
|
||||||
# route an edge request by. Asking the edge anyway can only reach
|
|
||||||
# the DEFAULT vhost - a different product - and a check that can
|
|
||||||
# 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 {
|
||||||
switch ($attempt.Kind) {
|
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
||||||
'exec' {
|
if ($r -and $r.build_version) {
|
||||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET $execCmd
|
if ([string]$r.build_version -eq $ExpectedLabel) {
|
||||||
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
Write-Host " PASS - live build $($r.build_version) matches expected." -ForegroundColor Green
|
||||||
}
|
|
||||||
'port' {
|
|
||||||
$raw = ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "curl -s -m 8 http://localhost:$($attempt.Port)$($attempt.Path)"
|
|
||||||
$r = ($raw | Out-String).Trim() | ConvertFrom-Json -ErrorAction Stop
|
|
||||||
}
|
|
||||||
'edge' {
|
|
||||||
$edgeHeaders = @{ 'Host' = $attempt.HostHeader }
|
|
||||||
$r = Invoke-RestMethod -Uri "http://$EC2_IP/api/build-version" -Headers $edgeHeaders -TimeoutSec 10 -ErrorAction Stop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
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
|
return $true
|
||||||
}
|
}
|
||||||
Write-Host " Live build is $live via $($attempt.Label), expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
Write-Host " Live build is $($r.build_version), expected $ExpectedLabel - waiting..." -ForegroundColor DarkYellow
|
||||||
break # one wrong-version read this pass is enough; retry after the sleep
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host " /api/build-version not ready yet - waiting..." -ForegroundColor DarkGray
|
||||||
}
|
}
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
}
|
}
|
||||||
# Say which failure this actually was: a version that never matched is a
|
Write-Host " WARNING: live build did not match $ExpectedLabel within ${TimeoutSec}s (a stale build may be cached)." -ForegroundColor Yellow
|
||||||
# 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -785,7 +385,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" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
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"
|
||||||
|
|
||||||
if ($hasVersionTool) {
|
if ($hasVersionTool) {
|
||||||
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
Write-Host "`n--- [4] Incrementing build version ---" -ForegroundColor Cyan
|
||||||
@ -813,7 +413,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.
|
||||||
Invoke-Ec2Step "record the live build number" "echo '$BuildVersion' | sudo tee $remotePath/.build_version > /dev/null"
|
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "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" }
|
||||||
@ -821,24 +421,10 @@ 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
|
||||||
# Through Invoke-Ec2Step, not a bare ssh: `docker compose restart`
|
ssh @SSH_OPTS -i $PEM_KEY $SSH_TARGET "cd $composeDir && sudo COMPOSE_BAKE=false docker compose restart $appSvc"
|
||||||
# writes " Container <name> Restarting" to STDERR as ordinary
|
if ($LASTEXITCODE -ne 0) { throw "App restart after build bump failed (exit $LASTEXITCODE)" }
|
||||||
# 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) {
|
||||||
@ -904,7 +490,7 @@ function Invoke-ViteDeploy {
|
|||||||
Write-Host "`n--- [1] Zipping site ---" -ForegroundColor Cyan
|
Write-Host "`n--- [1] Zipping site ---" -ForegroundColor Cyan
|
||||||
$preZipBuild = Read-JsonBuildVersion -FilePath (Join-Path $root "build-version.json")
|
$preZipBuild = Read-JsonBuildVersion -FilePath (Join-Path $root "build-version.json")
|
||||||
if ($preZipBuild) {
|
if ($preZipBuild) {
|
||||||
Write-Host " Pre-zip build label: $(Get-LabelFromBuildJsonObj $preZipBuild) (verification expects this exact label)" -ForegroundColor Gray
|
Write-Host " Pre-zip build label: $(Get-LabelFromBuildJsonObj $preZipBuild) (server-side build will bump +1)" -ForegroundColor Gray
|
||||||
}
|
}
|
||||||
New-ProjectArchive -SourcePath $root -DestinationZip $zipLocal -TopLevelExclude (Get-ArchiveExcludes -Project $Proj)
|
New-ProjectArchive -SourcePath $root -DestinationZip $zipLocal -TopLevelExclude (Get-ArchiveExcludes -Project $Proj)
|
||||||
|
|
||||||
@ -929,7 +515,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" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
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"
|
||||||
|
|
||||||
Wait-VerifyStaticBuild -Key $Key -Proj $Proj -PreZipBuildState $preZipBuild
|
Wait-VerifyStaticBuild -Key $Key -Proj $Proj -PreZipBuildState $preZipBuild
|
||||||
Invoke-Ec2PostDeployCleanup -Label $Key
|
Invoke-Ec2PostDeployCleanup -Label $Key
|
||||||
@ -1038,7 +624,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 at all. The
|
# for 19 hours with no container running 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.
|
||||||
#
|
#
|
||||||
@ -1055,14 +641,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" (Get-RecordDeployBash -Proj $Proj -RemotePath $remotePath -ZipName $zipName)
|
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"
|
||||||
|
|
||||||
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. See issue #28.
|
# for an app that was already up.
|
||||||
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) {
|
||||||
@ -1144,11 +730,10 @@ 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 the self-hosted
|
# only server-side state stays put. Skipping them is how self-hosted assets
|
||||||
# Chart.js and fonts silently never reached prod (charts rendered blank).
|
# silently never reach prod: docker creates empty mount-point dirs and nginx
|
||||||
# .pytest_cache is a local test artifact, already gitignored; it has no
|
# serves 404s from them, so fonts fall back and vendored JS never loads.
|
||||||
# business on the proxy box and only adds noise to the upload log.
|
$skipDirs = @('nginx-logs', '.git')
|
||||||
$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
|
||||||
@ -1168,48 +753,9 @@ 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 {
|
|
||||||
param([string]$Key, $Proj)
|
|
||||||
|
|
||||||
# Plain static sites - no build, no container of their own. The landing
|
|
||||||
# container serves them straight off disk, one directory per host, so
|
|
||||||
# shipping the files IS the deploy: there is nothing to restart afterwards.
|
|
||||||
$root = Join-Path $Proj.localRoot $Proj.siteDir
|
|
||||||
$remotePath = $Proj.remote.path
|
|
||||||
|
|
||||||
Write-Host "`n=== $($Proj.label) deploy (static files) ===" -ForegroundColor Cyan
|
|
||||||
if (-not (Test-Path -LiteralPath $root)) { throw "Static site root not found: $root" }
|
|
||||||
if (-not (Test-Path -LiteralPath (Join-Path $root 'index.html'))) { throw "Missing $root\index.html" }
|
|
||||||
|
|
||||||
Invoke-Ec2Step "ensure site dir" "sudo mkdir -p $remotePath && sudo chown -R ${Ec2User}:${Ec2User} $remotePath"
|
|
||||||
|
|
||||||
$files = @(Get-ChildItem -LiteralPath $root -File)
|
|
||||||
foreach ($f in $files) {
|
|
||||||
Write-Host " >> uploading $($f.Name)" -ForegroundColor DarkCyan
|
|
||||||
scp @SCP_OPTS -i $PEM_KEY $f.FullName "${SSH_TARGET}:$remotePath/"
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($f.Name) (exit $LASTEXITCODE)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# A large media file uploaded in place is served half-written to anyone who
|
|
||||||
# requests it mid-copy. Ship the directory to a sibling, then swap it in.
|
|
||||||
$skipDirs = @('.git', '.pytest_cache', 'node_modules')
|
|
||||||
$dirs = @(Get-ChildItem -LiteralPath $root -Directory | Where-Object { $skipDirs -notcontains $_.Name })
|
|
||||||
foreach ($d in $dirs) {
|
|
||||||
Write-Host " >> uploading $($d.Name)/ (recursive, staged)" -ForegroundColor DarkCyan
|
|
||||||
Invoke-Ec2Step "stage $($d.Name)" "rm -rf $remotePath/.staging-$($d.Name) && mkdir -p $remotePath/.staging-$($d.Name)"
|
|
||||||
scp -r @SCP_OPTS -i $PEM_KEY "$($d.FullName)/*" "${SSH_TARGET}:$remotePath/.staging-$($d.Name)/"
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-DockerDeploy {
|
function Invoke-DockerDeploy {
|
||||||
param([string]$Key, $Proj)
|
param([string]$Key, $Proj)
|
||||||
|
|
||||||
@ -1230,46 +776,19 @@ function Invoke-DockerDeploy {
|
|||||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($f.Name) (exit $LASTEXITCODE)" }
|
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($f.Name) (exit $LASTEXITCODE)" }
|
||||||
}
|
}
|
||||||
|
|
||||||
# Config subdirectories ship too, exactly as the edge kind does. Uploading
|
Invoke-Ec2Step "docker compose pull" "cd $remotePath && sudo docker compose pull"
|
||||||
# top-level files ONLY was silently wrong for any stack that keeps config in
|
|
||||||
# a directory: the compose file arrives, the containers restart, and the
|
|
||||||
# config they read is whatever was already on the box. That is worse than a
|
|
||||||
# failed deploy, because it reports success - a provisioning directory read
|
|
||||||
# at container start (alert rules, datasources, mounted *.php) would never
|
|
||||||
# reflect the change you just deployed.
|
|
||||||
#
|
|
||||||
# Skipped: $JunkDirNames (.git, __pycache__, .pytest_cache, ...) plus
|
|
||||||
# anything the project lists in deploy.skipDirs. That list is how a stack
|
|
||||||
# protects SERVER-SIDE STATE that happens to share the tree - a data/ holding
|
|
||||||
# mailboxes or a time-series database must never be overwritten by whatever
|
|
||||||
# the local checkout has (usually nothing, which is the dangerous case).
|
|
||||||
# .github is CI config - it belongs in the repo and never on a deploy
|
|
||||||
# target. It is not in $JunkDirNames because backups DO want it.
|
|
||||||
$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 })
|
|
||||||
foreach ($d in $dirs) {
|
|
||||||
Write-Host " >> uploading $($d.Name)/ (recursive)" -ForegroundColor DarkCyan
|
|
||||||
scp -r @SCP_OPTS -i $PEM_KEY $d.FullName "${SSH_TARGET}:$remotePath/"
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "SCP failed for $($d.Name) (exit $LASTEXITCODE)" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# 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. Runs
|
# Pseudo-project "ztokens": not a zconfig entry, no compose stack, and entirely
|
||||||
# `ztokens -Publish` from the sibling ztokens repo so the public zscripts page
|
# optional. Runs `ztokens -Publish` from a sibling ztokens checkout so a site
|
||||||
# has current data. A failure here warns rather than aborting the rest of the
|
# that charts usage data has something current to ship. Missing checkout, or a
|
||||||
# deploy list - it's a nice-to-have refresh, not a deploy step.
|
# failure, warns rather than aborting the rest of the deploy list - it is a
|
||||||
|
# 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"
|
||||||
@ -1304,16 +823,6 @@ 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
|
||||||
@ -1327,16 +836,16 @@ try {
|
|||||||
"nextjs" { Invoke-NextDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
"nextjs" { Invoke-NextDeploy -Key $key -Proj $proj -ChangeNote $Note }
|
||||||
"edge" { Invoke-EdgeDeploy -Key $key -Proj $proj }
|
"edge" { Invoke-EdgeDeploy -Key $key -Proj $proj }
|
||||||
"docker" { Invoke-DockerDeploy -Key $key -Proj $proj }
|
"docker" { Invoke-DockerDeploy -Key $key -Proj $proj }
|
||||||
"static" { Invoke-StaticDeploy -Key $key -Proj $proj }
|
|
||||||
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 {
|
Stop-ZTracking
|
||||||
[Console]::OutputEncoding = $prevConsoleEncoding
|
|
||||||
}
|
# Last line of the run, after the tracking footer: scroll to the bottom and you
|
||||||
# The timestamp goes through Stop-ZTracking as the FinalNote so it lands after
|
# can see how long ago this deployed. Only reached on success - a failed deploy
|
||||||
# the tracking footer and before the trailing blank lines - the last thing on
|
# throws out of the loop above, so this never claims a deploy that didn't happen.
|
||||||
# screen, which is the point: scroll to the bottom and you can see how long ago
|
Write-Host ""
|
||||||
# this deployed. Only reached on success; a failed deploy throws out of the loop
|
Write-Host ("Last deployed at {0}" -f (Get-Date -Format "MM/dd/yyyy hh:mm:ss tt")) -ForegroundColor Cyan
|
||||||
# above, so this never claims a deploy that didn't happen.
|
# Two blank lines below, so the timestamp isn't crowded by the next prompt.
|
||||||
Stop-ZTracking -FinalNote ("Last deployed at {0}" -f (Get-Date -Format "MM/dd/yyyy hh:mm:ss tt"))
|
Write-Host ""
|
||||||
|
Write-Host ""
|
||||||
|
|||||||
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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zec2.ps1" %*
|
||||||
|
|||||||
25
zec2.ps1
25
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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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,13 +22,6 @@ 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. zec2 had no ssh of its own
|
|
||||||
# before that, so the read referenced three variables this script never
|
|
||||||
# defined -- and because it sits inside a try/catch, the failure was silent:
|
|
||||||
# it fell through to the HTTP call and reported nothing once that endpoint
|
|
||||||
# stopped being public. Same names zec2online.ps1 uses.
|
|
||||||
$PemKey = $cfg.ec2.pemKey
|
|
||||||
$SshTarget = Get-Ec2Target
|
|
||||||
|
|
||||||
if ($Projects.Count -eq 0) {
|
if ($Projects.Count -eq 0) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
@ -105,20 +98,8 @@ 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: the
|
|
||||||
# endpoint is not public on every project, and asking the edge
|
|
||||||
# answers from whichever vhost matches the Host header.
|
|
||||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
|
||||||
$label = $null
|
|
||||||
if ($execCmd -and (Test-Path $PemKey)) {
|
|
||||||
$raw = (ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 -i $PemKey $SshTarget $execCmd | Out-String)
|
|
||||||
$label = Get-LabelFromVersionJson $raw
|
|
||||||
}
|
|
||||||
if (-not $label) {
|
|
||||||
$r = Invoke-RestMethod -Uri "http://${HostName}/api/build-version" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
$r = Invoke-RestMethod -Uri "http://${HostName}/api/build-version" -Headers $headers -TimeoutSec 10 -ErrorAction Stop
|
||||||
if ($r -and $r.build_version) { $label = [string]$r.build_version }
|
if ($r -and $r.build_version) { Write-Host " Live build: $($r.build_version)" -ForegroundColor Gray }
|
||||||
}
|
|
||||||
if ($label) { Write-Host " Live build: $label" -ForegroundColor Gray }
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host " (Could not read live version endpoint - see 'Enabling deploy verification' in README)" -ForegroundColor DarkGray
|
Write-Host " (Could not read live version endpoint - see 'Enabling deploy verification' in README)" -ForegroundColor DarkGray
|
||||||
|
|||||||
@ -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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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,18 +86,6 @@ 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. The endpoint is
|
|
||||||
# not public on every project, and the edge answers from whichever vhost
|
|
||||||
# matches the Host header -- which is how a check reads another
|
|
||||||
# product's version.
|
|
||||||
$execCmd = Get-ServerSideVersionCommand -Proj $Proj
|
|
||||||
if ($execCmd -and (Test-Path $PemKey)) {
|
|
||||||
try {
|
|
||||||
$raw = ssh -o StrictHostKeyChecking=no -o ConnectTimeout=15 -i $PemKey $SshTarget $execCmd
|
|
||||||
$label = Get-LabelFromVersionJson ($raw | Out-String)
|
|
||||||
if ($label) { return $label }
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if ($Proj.kind -eq 'vite') {
|
if ($Proj.kind -eq 'vite') {
|
||||||
$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
|
||||||
|
|||||||
@ -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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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
251
zmerge.ps1
@ -1,251 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
@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
359
zpull.ps1
@ -1,359 +0,0 @@
|
|||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zrelease.ps1" %*
|
||||||
|
|||||||
25
zrelease.ps1
25
zrelease.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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# zrelease.ps1 - package the current version as a downloadable zip.
|
# zrelease.ps1 - package the current version as a downloadable zip.
|
||||||
#
|
#
|
||||||
@ -40,25 +40,16 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
# Two blank lines after this script's output, matching every other z-script, so
|
|
||||||
# a run is visually separated from the next prompt. Local rather than from
|
|
||||||
# ZHelpers: this script is deliberately standalone, so it can run from an
|
|
||||||
# extracted release zip with nothing beside it.
|
|
||||||
function Write-ZTrailer { Write-Host ""; Write-Host "" }
|
|
||||||
|
|
||||||
|
|
||||||
$ReleasesDir = Join-Path $PSScriptRoot "releases"
|
$ReleasesDir = Join-Path $PSScriptRoot "releases"
|
||||||
$VersionFile = Join-Path $PSScriptRoot "build-version.json"
|
$VersionFile = Join-Path $PSScriptRoot "build-version.json"
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $VersionFile)) {
|
if (-not (Test-Path -LiteralPath $VersionFile)) {
|
||||||
Write-Host "ERROR: build-version.json not found. Run 'zversion set v1.0.0.0.0' first." -ForegroundColor Red
|
Write-Host "ERROR: build-version.json not found. Run 'zversion set v1.0.0.0.0' first." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$version = (Get-Content -LiteralPath $VersionFile -Raw -Encoding UTF8 | ConvertFrom-Json).version
|
$version = (Get-Content -LiteralPath $VersionFile -Raw -Encoding UTF8 | ConvertFrom-Json).version
|
||||||
if ($version -notmatch '^v\d+\.\d+\.\d+\.\d+\.\d+$') {
|
if ($version -notmatch '^v\d+\.\d+\.\d+\.\d+\.\d+$') {
|
||||||
Write-Host "ERROR: build-version.json holds an invalid version '$version'." -ForegroundColor Red
|
Write-Host "ERROR: build-version.json holds an invalid version '$version'." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +65,10 @@ function Get-Sha256([string]$Path) {
|
|||||||
if ($Verify) {
|
if ($Verify) {
|
||||||
if (-not (Test-Path -LiteralPath $zipPath)) {
|
if (-not (Test-Path -LiteralPath $zipPath)) {
|
||||||
Write-Host "ERROR: $zipName not found in releases/." -ForegroundColor Red
|
Write-Host "ERROR: $zipName not found in releases/." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
if (-not (Test-Path -LiteralPath $shaPath)) {
|
if (-not (Test-Path -LiteralPath $shaPath)) {
|
||||||
Write-Host "ERROR: $zipName.sha256 not found." -ForegroundColor Red
|
Write-Host "ERROR: $zipName.sha256 not found." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$expected = ((Get-Content -LiteralPath $shaPath -Raw) -split '\s+')[0].ToLowerInvariant()
|
$expected = ((Get-Content -LiteralPath $shaPath -Raw) -split '\s+')[0].ToLowerInvariant()
|
||||||
@ -88,11 +77,11 @@ if ($Verify) {
|
|||||||
Write-Host "=== zrelease (verify) ===" -ForegroundColor Cyan
|
Write-Host "=== zrelease (verify) ===" -ForegroundColor Cyan
|
||||||
if ($actual -eq $expected) {
|
if ($actual -eq $expected) {
|
||||||
Write-Host " OK - $zipName matches its .sha256." -ForegroundColor Green
|
Write-Host " OK - $zipName matches its .sha256." -ForegroundColor Green
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
Write-Host " FAILED - $zipName does not match its .sha256." -ForegroundColor Red
|
Write-Host " FAILED - $zipName does not match its .sha256." -ForegroundColor Red
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +91,7 @@ if ((Test-Path -LiteralPath $zipPath) -and -not $Force) {
|
|||||||
Write-Host "ERROR: releases/$zipName already exists." -ForegroundColor Red
|
Write-Host "ERROR: releases/$zipName already exists." -ForegroundColor Red
|
||||||
Write-Host " A released version is immutable - bump instead: zversion bump" -ForegroundColor DarkGray
|
Write-Host " A released version is immutable - bump instead: zversion bump" -ForegroundColor DarkGray
|
||||||
Write-Host " (or pass -Force if you are rebuilding one that was never published)" -ForegroundColor DarkGray
|
Write-Host " (or pass -Force if you are rebuilding one that was never published)" -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +101,7 @@ if ($LASTEXITCODE -ne 0) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "ERROR: checksum verification failed - refusing to package." -ForegroundColor Red
|
Write-Host "ERROR: checksum verification failed - refusing to package." -ForegroundColor Red
|
||||||
Write-Host " Run 'zchecksums' to see what differs, then 'zversion bump' to restamp." -ForegroundColor DarkGray
|
Write-Host " Run 'zchecksums' to see what differs, then 'zversion bump' to restamp." -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,5 +142,5 @@ Write-Host " SHA-256: $hash" -ForegroundColor Gray
|
|||||||
Write-Host " Digest: releases/$zipName.sha256" -ForegroundColor Gray
|
Write-Host " Digest: releases/$zipName.sha256" -ForegroundColor Gray
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " Commit both files - a release lives in the repo under releases/." -ForegroundColor DarkGray
|
Write-Host " Commit both files - a release lives in the repo under releases/." -ForegroundColor DarkGray
|
||||||
Write-ZTrailer
|
Write-Host ""
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@ -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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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,15 +198,21 @@ 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) {
|
if ($Proj.start.gitPull -and (Test-Path (Join-Path $Proj.localRoot ".git"))) {
|
||||||
# Never lets a pull stand between the user and a running server: the
|
Push-Location -LiteralPath $Proj.localRoot
|
||||||
# helper reports, it does not throw (#130). The inline version this
|
# GIT_TERMINAL_PROMPT=0 so a repo that needs credentials fails fast
|
||||||
# replaced aborted on git's ordinary stderr progress under Stop.
|
# instead of blocking the server start on a "Username for ..." prompt.
|
||||||
$pull = Invoke-StartGitPull -Root $Proj.localRoot
|
$prev = $env:GIT_TERMINAL_PROMPT; $env:GIT_TERMINAL_PROMPT = "0"
|
||||||
if ($pull.Ok) {
|
try {
|
||||||
Write-Host " git pull: $($pull.Message)" -ForegroundColor DarkGray
|
$pullOut = git pull --ff-only 2>&1
|
||||||
} else {
|
$last = ($pullOut | Select-Object -Last 1)
|
||||||
Write-Host " Auto-pull skipped - starting with the current checkout. ($($pull.Message); set start.gitPull=false to stop trying)" -ForegroundColor Yellow
|
Write-Host " git pull: $last" -ForegroundColor DarkGray
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host " Auto-pull skipped - starting with the current checkout. (git needs credentials here, or set start.gitPull=false)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$env:GIT_TERMINAL_PROMPT = $prev
|
||||||
|
Pop-Location
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@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.28
|
# Version: v1.0.0.0.14
|
||||||
|
|
||||||
# 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.28
|
REM Version: v1.0.0.0.14
|
||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0zversion.ps1" %*
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user