- layers: node --test runs checked with a TAP detector (self-tested), every vue-seo-layer smoke, guard:check and purge:check, all five journal expectation files (postTitleTag h1 and p), the journal purge checks and check-git-install.sh --self-test - consumers: the 33-12 and 33-13 app patches pass git apply --check, and the 33-14 demo locale patches apply on top of the 33-12 ones in scratch worktrees
1396 lines
56 KiB
Bash
Executable File
1396 lines
56 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Phase 33 verification gate: the framework's additive content facts and
|
||
# per-request field type options (plan 33-01), the sitemap, SEO and Journal
|
||
# plugins, the four Nuxt layers and the applications that consume them.
|
||
#
|
||
# Stages:
|
||
# self-test the go test -json, vitest and TAP detectors on planted output, a
|
||
# failing go test in a planted module, the marker rule on a
|
||
# planted repository, the missing-repository failure, the docker
|
||
# NOT RUN rule on a faked host, and the additive comparison on
|
||
# planted API pairs (a removal and an interface method added
|
||
# are refused).
|
||
# go go vet ./..., go test -short ./... and the Docker-free
|
||
# framework tests of the phase by required name (GO_TESTS).
|
||
# plugins sm-sitemap-plugin, sm-seo-plugin and sm-journal-plugin: the
|
||
# workspace check (the framework resolves to this checkout),
|
||
# go vet, go test -short and the unit tests of plan 33-14 by
|
||
# required name (*_UNIT_TESTS); npm run test (vitest) in
|
||
# sm-seo-plugin and sm-journal-plugin.
|
||
# layers vue-seo-layer, vue-journal-layer, vue-menu-layer and
|
||
# vue-faq-layer: npm run test (node --test, an all-passing TAP
|
||
# summary), npm run build:playground and the layer's SSR
|
||
# smokes; in vue-seo-layer guard:check (the build-guard
|
||
# fixtures) and purge:check; in vue-journal-layer every
|
||
# expectation file (expect-acme-title-h1 and -title-p with
|
||
# NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG), the purge checks and
|
||
# check-git-install.sh --self-test.
|
||
# consumers sm-demo-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app and
|
||
# sm-bm-app: the workspace check and go build ./...; npm run
|
||
# build in each Nuxt app (vue-*/ with a build script); the
|
||
# 33-12 and 33-13 app patches pass git apply --check, and the
|
||
# 33-14 demo locale patches apply on top of the 33-12 ones.
|
||
# docker Docker host only: the Postgres suites (go test ./... without
|
||
# -short) of the three Go plugins with the phase's suites by
|
||
# required name (*_DOCKER_TESTS; TestChromeRenderer too when a
|
||
# Chrome or Chromium binary is on the host, else it is printed
|
||
# as NOT RUN), and the framework's modules/lagoon/attach/...
|
||
# admin scripts/check-admin-openapi.sh --check and
|
||
# scripts/check-admin-dist.sh.
|
||
# docs TestDocsTree and summer docs:build --check.
|
||
# marker D-90: the lines added since the release before the phase
|
||
# (PHASE33_BASE, v0.6.0) to non-test Go files, READMEs and docs
|
||
# under modules/ and docs/ name neither SEO, a sitemap, JSON-LD
|
||
# nor a consuming application.
|
||
# additive D-106 (the release is a patch tag): the exported API of
|
||
# modules/pact, modules/cabana, modules/signpost and every other
|
||
# package under modules/ changed since the newest tag that does
|
||
# not contain HEAD, printed by scripts/apisurface for a worktree
|
||
# of that tag and for this checkout, keeps every line of the tag
|
||
# (no removed declaration, field or method, no changed
|
||
# signature, field type, struct tag, constant value or
|
||
# underlying type) and adds no method or embed to an interface
|
||
# that exists at the tag.
|
||
# compat existing consumers build unchanged against this checkout,
|
||
# each through a scratch go.work in a scratch sibling layout:
|
||
# sm-sitemap-plugin and sm-journal-plugin at their releases
|
||
# before the phase and sm-demo-app at its commit before the
|
||
# phase with its pinned submodules (build, the demo also with
|
||
# -tags summercms_mcp, and short tests); every other sibling
|
||
# that requires the framework builds at HEAD with its pinned
|
||
# submodules ("compat: ok <repo> (build)"), or fails at the
|
||
# newest tag too ("compat: skipped <repo> (fails at <tag> too)").
|
||
#
|
||
# A stage whose repository or directory is missing fails with
|
||
# "missing <path>". A Go stage exits non-zero on a failing command, or on a
|
||
# go test -json run that fails, skips a test (a subtest included), matches
|
||
# zero tests, prints "no tests to run" or misses a required name.
|
||
#
|
||
# Several stages may be given at once (--go --plugins ...). --all runs
|
||
# self-test, go, plugins, layers, consumers, admin, docs and marker, then
|
||
# docker when a Docker daemon answers (NOT RUN docker otherwise, a failure
|
||
# with PHASE33_REQUIRE_DOCKER=1), then additive and compat. Without an
|
||
# argument the script prints its usage and exits 2.
|
||
#
|
||
# Environment overrides (default: the sibling checkouts next to this
|
||
# repository): PHASE33_ROOT, SITEMAP_PLUGIN, SEO_PLUGIN, JOURNAL_PLUGIN,
|
||
# SEO_LAYER, JOURNAL_LAYER, MENU_LAYER, FAQ_LAYER, DEMO_APP, GRZYBY_APP,
|
||
# SUMMERCMSIO_APP, BM_APP, PHASE33_BASE (the framework release before the
|
||
# phase), PHASE33_SITEMAP_BASE, PHASE33_JOURNAL_BASE and PHASE33_DEMO_BASE
|
||
# (the consumer revisions before the phase, for compat),
|
||
# PHASE33_REQUIRE_DOCKER, PHASE33_SKIP_TESTS (a go test -skip pattern for
|
||
# the compat stage's short tests, for a test that cannot run on this host;
|
||
# each use is printed).
|
||
set -euo pipefail
|
||
|
||
unset FORCE_COLOR
|
||
export NO_COLOR=1
|
||
|
||
ROOT="${PHASE33_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||
PARENT="$(dirname "$ROOT")"
|
||
SITEMAP_PLUGIN="${SITEMAP_PLUGIN:-$PARENT/sm-sitemap-plugin}"
|
||
SEO_PLUGIN="${SEO_PLUGIN:-$PARENT/sm-seo-plugin}"
|
||
JOURNAL_PLUGIN="${JOURNAL_PLUGIN:-$PARENT/sm-journal-plugin}"
|
||
SEO_LAYER="${SEO_LAYER:-$PARENT/vue-seo-layer}"
|
||
JOURNAL_LAYER="${JOURNAL_LAYER:-$PARENT/vue-journal-layer}"
|
||
MENU_LAYER="${MENU_LAYER:-$PARENT/vue-menu-layer}"
|
||
FAQ_LAYER="${FAQ_LAYER:-$PARENT/vue-faq-layer}"
|
||
DEMO_APP="${DEMO_APP:-$PARENT/sm-demo-app}"
|
||
GRZYBY_APP="${GRZYBY_APP:-$PARENT/sm-grzybyfunkcjonalne-app}"
|
||
SUMMERCMSIO_APP="${SUMMERCMSIO_APP:-$PARENT/sm-summercmsio-app}"
|
||
BM_APP="${BM_APP:-$PARENT/sm-bm-app}"
|
||
# The framework release before the phase (33-01, Jakub 2026-11-04: the phase
|
||
# delivers v0.6.1 on top of v0.6.0).
|
||
PHASE33_BASE="${PHASE33_BASE:-v0.6.0}"
|
||
# The consumer revisions before the phase, for the compat stage: the
|
||
# sitemap and Journal releases and the demo's HEAD when 33-01 ran.
|
||
PHASE33_SITEMAP_BASE="${PHASE33_SITEMAP_BASE:-v0.1.2}"
|
||
PHASE33_JOURNAL_BASE="${PHASE33_JOURNAL_BASE:-v0.3.1}"
|
||
PHASE33_DEMO_BASE="${PHASE33_DEMO_BASE:-da481c997062adcd156d8ae9c0ae8e01feb93d17}"
|
||
|
||
MCP_TAG="summercms_mcp"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Required test names: <package>:<TestName>.
|
||
|
||
# go stage: Docker-free framework tests (-short) of plan 33-01.
|
||
GO_TESTS=(
|
||
./modules/signpost:TestContentDescriptorFields ./modules/signpost:TestContentLookups
|
||
./modules/cabana:TestFieldTypeRequestOptions ./modules/cabana:TestFormExtensionUnextendedSchemaUnchanged
|
||
./modules/cabana:TestMergeRequestOptions ./modules/cabana:TestMergeRequestOptionsNoFields
|
||
./modules/pact:TestContentKindAndToolValues ./modules/pact:TestContentRecordZeroFacts ./modules/pact:TestContentValueTypes
|
||
./modules/lagoon/attach:TestVariantKeyTable ./modules/lagoon/attach:TestVariantKeyOfThumbFilename
|
||
./modules/lagoon/attach/webp:TestEncodeImageKinds ./modules/lagoon/attach/webp:TestEncodeConstants
|
||
./modules/lagoon/attach/webp:TestVariantStoredWithContentType ./modules/lagoon/attach/webp:TestVariantBrokenAndRefused
|
||
)
|
||
|
||
# plugins stage: the Docker-free unit tests of plan 33-14 by required name
|
||
# (-short), per plugin.
|
||
SITEMAP_UNIT_TESTS=(
|
||
./classes:TestWriteURLSet ./classes:TestWriteIndex ./classes:TestWriteImageURLSet ./classes:TestFileNamesRoundTrip ./classes:TestValidSlug
|
||
./classes:TestAssignSlots ./classes:TestAssignSlotsEdges ./classes:TestRealChanges ./classes:TestLastmodIgnore ./classes:TestRebuildDelay
|
||
./classes:TestNewsLimits ./classes:TestParseNewsSources ./classes:TestNewsSourcesMatch ./classes:TestNewsLanguage ./classes:TestWriteNewsURLSet
|
||
./classes:TestIndexNowBatches ./classes:TestIndexNowOutcome ./classes:TestIndexNowKeys ./classes:TestSiteURLs
|
||
./classes:TestHTMLGroupOrder ./classes:TestTreeOrder ./classes:TestNestPaging ./classes:TestToEntries
|
||
./classes:TestDefaultSlug ./classes:TestEffectiveType ./classes:TestMarkListedExtras
|
||
)
|
||
SEO_UNIT_TESTS=(
|
||
./classes/schema:TestDefaultAndAllowedTypes ./classes/schema:TestPageIDs ./classes/schema:TestBuildArticle ./classes/schema:TestBuildPerKind
|
||
./classes/schema:TestPublisherNode ./classes/schema:TestValidateNodeRules ./classes/schema:TestDecodeCustom ./classes/schema:TestNormalize ./classes/schema:TestMerge
|
||
./classes:TestParsePage ./classes:TestPageLocation ./classes:TestAlternates ./classes:TestPrimaryParent ./classes:TestCandidatePath
|
||
./classes:TestValidPagePath ./classes:TestApplyPageFields ./classes:TestPageGroupAndMissing ./classes:TestPageBeforeSaveUTC ./classes:TestValidAutolinkURL
|
||
./classes/crawl:TestParseFacts ./classes/crawl:TestCheckStatus ./classes/crawl:TestCheckHead ./classes/crawl:TestCheckHeadings ./classes/crawl:TestCheckJSONLD
|
||
./classes/crawl:TestCheckImages ./classes/crawl:TestCheckContentCMS ./classes/crawl:TestRoute ./classes/crawl:TestRouteEveryKind
|
||
./classes/crawl:TestCompareRendered ./classes/crawl:TestAllowRequest ./classes/crawl:TestResolverRules ./classes/crawl:TestNewRenderer
|
||
)
|
||
JOURNAL_UNIT_TESTS=(
|
||
./classes:TestConvertH1 ./classes:TestConvertContentH1 ./classes:TestConvertTranslationH1 ./classes:TestSetImageAlt ./classes:TestSetImageAltEdges
|
||
./classes:TestParseContentFacts ./classes:TestParseContentFactsEmpty ./classes:TestTopHeading ./classes:TestReadRelatedSettings
|
||
)
|
||
|
||
# docker stage: the Postgres suites of the phase by required name, per
|
||
# plugin (each plugin's go test ./... must pass them). TestChromeRenderer is
|
||
# required when a Chrome or Chromium binary is on this host.
|
||
SITEMAP_DOCKER_TESTS=(TestSitemapIndex TestExtraDuplicates TestImageSitemap TestNewsSitemap TestIndexNow TestHTMLGroups TestFilterExcludes TestHreflangAlternates TestLastmod TestMigrateDefinitions TestSitemapTools TestRebuildCommand)
|
||
SEO_DOCKER_TESTS=(TestRegistry TestRegistryTimestampsUTC TestRouteReport TestSiteEndpoint TestSitemapFilterTracer TestFilterReasons TestGraphTracer TestGraphPerKind TestPagination TestPagedAlternates TestCustomJSONLD TestCrawlerTracer TestCrawlerChecks TestRenderSkipped TestAutolinks TestLinkSuggestions)
|
||
JOURNAL_DOCKER_TESTS=(TestHeadingConversionTracer TestHeadingConversion TestFixHeadingsCommand TestSetImageAlt TestJournalDescriptorFields TestImageVariants TestImageVariantsCommandCLI TestJournalAPIShowNeighbors)
|
||
|
||
# marker stage: SEO words and the names of consuming applications (split so
|
||
# this file does not match itself).
|
||
MARKER_WORDS=("\\bs""eo\\b" "site""map" "json-?l""d")
|
||
APP_NAMES=("p[lł]yta""rium" "fono""teka" "wave""path" "horo""skopia" "bm\\.jakub""zych" "grzy""by" "summercms""io" "demo\\.summer""cms" "stu""dia")
|
||
|
||
usage() {
|
||
cat >&2 <<'EOF'
|
||
usage:
|
||
check-phase33.sh --self-test
|
||
check-phase33.sh [--go] [--plugins] [--layers] [--consumers] [--docker] [--admin] [--docs] [--marker] [--additive] [--compat]
|
||
check-phase33.sh --all
|
||
EOF
|
||
exit 2
|
||
}
|
||
|
||
# detect_go reads go test -json. Exit 1 fail/build, 2 skip, 3 zero/no-tests,
|
||
# 4 non-JSON, 5 missing required name (REQUIRE_TESTS, space-separated).
|
||
detect_go() {
|
||
python3 - "$1" <<'PY'
|
||
import json, os, sys
|
||
path = sys.argv[1]
|
||
require = [n for n in os.environ.get("REQUIRE_TESTS", "").split() if n]
|
||
passed = set()
|
||
failed = []
|
||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||
for raw in fh:
|
||
line = raw.strip()
|
||
if not line.startswith("{"):
|
||
continue
|
||
try:
|
||
ev = json.loads(line)
|
||
except json.JSONDecodeError:
|
||
print("refuse: non-json test output", file=sys.stderr)
|
||
sys.exit(4)
|
||
action = ev.get("Action")
|
||
test = ev.get("Test") or ""
|
||
pkg = ev.get("Package") or ev.get("ImportPath") or ""
|
||
if action == "build-fail" or (action == "fail" and ev.get("FailedBuild")):
|
||
print(f"refuse: build failed {pkg}", file=sys.stderr)
|
||
sys.exit(1)
|
||
text = ev.get("Output") or ""
|
||
if action == "output" and "no tests to run" in text:
|
||
print(f"refuse: no tests to run in {pkg}", file=sys.stderr)
|
||
sys.exit(3)
|
||
if action == "skip" and test:
|
||
print(f"refuse: skipped {pkg} {test}", file=sys.stderr)
|
||
sys.exit(2)
|
||
if action == "fail":
|
||
failed.append(f"{pkg} {test}".strip())
|
||
if action == "pass" and test:
|
||
passed.add(test)
|
||
if failed:
|
||
print("refuse: failed " + ", ".join(failed), file=sys.stderr)
|
||
sys.exit(1)
|
||
if not passed:
|
||
print("refuse: zero tests", file=sys.stderr)
|
||
sys.exit(3)
|
||
missing = [n for n in require if n not in passed]
|
||
if missing:
|
||
print("refuse: required tests did not pass: " + ", ".join(missing), file=sys.stderr)
|
||
sys.exit(5)
|
||
PY
|
||
}
|
||
|
||
# detect_vitest reads a vitest run's output. Exit 1 failed, skipped or todo
|
||
# tests, 3 no all-passing summary, 5 a required file (REQUIRE_FILES) did
|
||
# not pass.
|
||
detect_vitest() {
|
||
python3 - "$1" <<'PY'
|
||
import os, re, sys
|
||
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
||
need = os.environ.get("REQUIRE_FILES", "").split()
|
||
summary = re.findall(r"^\s*(Test Files|Tests)\s+(.*)$", text, re.M)
|
||
if any(re.search(r"\b(failed|skipped|todo)\b", rest) for _, rest in summary) or re.search(r"^\s*(FAIL|×)\s", text, re.M):
|
||
print("refuse: vitest reports failed, skipped or todo tests", file=sys.stderr)
|
||
sys.exit(1)
|
||
m = re.search(r"^\s*Tests\s+(\d+) passed \((\d+)\)\s*$", text, re.M)
|
||
if not m or m.group(1) != m.group(2) or m.group(1) == "0":
|
||
print("refuse: no all-passing vitest summary", file=sys.stderr)
|
||
sys.exit(3)
|
||
missing = [f for f in need if not re.search(r"^\s*✓ (?:\|\w+\| )?" + re.escape(f) + r" \(\d+ tests?\)", text, re.M)]
|
||
if missing:
|
||
print("refuse: did not pass: " + ", ".join(missing), file=sys.stderr)
|
||
sys.exit(5)
|
||
PY
|
||
}
|
||
|
||
# go_json DIR ARGS... runs go test -json ARGS in DIR and checks the stream
|
||
# with detect_go.
|
||
go_json() {
|
||
local dir="$1"
|
||
shift
|
||
local log err rc=0 dc=0
|
||
log="$(mktemp)"
|
||
err="$(mktemp)"
|
||
(cd "$dir" && go test -json "$@") >"$log" 2>"$err" || rc=$?
|
||
detect_go "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
cat "$err" >&2 || true
|
||
python3 -c 'import json,sys
|
||
for raw in open(sys.argv[1], errors="replace"):
|
||
try: ev = json.loads(raw)
|
||
except ValueError: continue
|
||
out = ev.get("Output", "")
|
||
if ev.get("Action") == "output" and ("FAIL" in out or "SKIP" in out or "_test.go:" in out):
|
||
sys.stderr.write(out)' "$log" 2>&1 | tail -n 40 >&2 || true
|
||
rm -f "$log" "$err"
|
||
echo "refuse: go test $* in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
echo "go: $(grep -c '"Action":"pass","Package":"[^"]*","Test":"[^"/]*","Elapsed"' "$log") top-level tests passed in $dir ($*)"
|
||
rm -f "$log" "$err"
|
||
}
|
||
|
||
# go_named DIR PACKAGE FLAGS NAME... runs exactly the named top-level tests
|
||
# and requires every one to pass. FLAGS is word-split.
|
||
go_named() {
|
||
local dir="$1" pkg="$2" flags="$3"
|
||
shift 3
|
||
if [[ "$#" -eq 0 ]]; then
|
||
echo "refuse: go_named $pkg in $dir without required names" >&2
|
||
return 1
|
||
fi
|
||
local names=("$@") regex
|
||
regex="^($(IFS='|'; echo "${names[*]}"))\$"
|
||
# shellcheck disable=SC2086
|
||
REQUIRE_TESTS="${names[*]}" go_json "$dir" $flags -count=1 -run "$regex" "$pkg"
|
||
}
|
||
|
||
# go_pkg_named DIR FLAGS ENTRY... groups <package>:<TestName> entries by
|
||
# package and runs go_named once per package.
|
||
go_pkg_named() {
|
||
local dir="$1" flags="$2"
|
||
shift 2
|
||
local entry pkg name pkgs=()
|
||
declare -A by=()
|
||
for entry in "$@"; do
|
||
pkg="${entry%%:*}"
|
||
name="${entry#*:}"
|
||
if [[ -z "$pkg" || -z "$name" || "$pkg" == "$entry" ]]; then
|
||
echo "refuse: malformed required test entry $entry" >&2
|
||
return 1
|
||
fi
|
||
if [[ -z "${by[$pkg]+x}" ]]; then
|
||
pkgs+=("$pkg")
|
||
by[$pkg]="$name"
|
||
else
|
||
by[$pkg]="${by[$pkg]} $name"
|
||
fi
|
||
done
|
||
for pkg in "${pkgs[@]}"; do
|
||
# shellcheck disable=SC2086
|
||
go_named "$dir" "$pkg" "$flags" ${by[$pkg]}
|
||
done
|
||
}
|
||
|
||
# go_short DIR [FLAGS] runs go test -short ./... in DIR.
|
||
go_short() {
|
||
local dir="$1" flags="${2:-}" out
|
||
# shellcheck disable=SC2086
|
||
if ! out="$(cd "$dir" && go test $flags -count=1 -short ./... 2>&1)"; then
|
||
printf '%s\n' "$out" | grep -v '^ok ' | tail -n 60 >&2
|
||
echo "refuse: go test -short ./... $flags in $dir" >&2
|
||
return 1
|
||
fi
|
||
echo "short: $dir ($flags)"
|
||
}
|
||
|
||
go_vet() {
|
||
local dir="$1"
|
||
shift
|
||
(cd "$dir" && go vet "$@")
|
||
echo "vet: $dir ($*)"
|
||
}
|
||
|
||
|
||
|
||
# require_dir PATH WHAT refuses a repository or directory that is missing,
|
||
# naming it ("missing <path>").
|
||
require_dir() {
|
||
[[ -d "$1" ]] || {
|
||
echo "refuse: missing $1 ($2)" >&2
|
||
return 1
|
||
}
|
||
}
|
||
|
||
# require_file PATH WHAT refuses a file that is missing.
|
||
require_file() {
|
||
[[ -f "$1" ]] || {
|
||
echo "refuse: missing $1 ($2)" >&2
|
||
return 1
|
||
}
|
||
}
|
||
|
||
# workspace_check DIR refuses a repository that does not resolve the
|
||
# framework to this checkout (no trust in local go.work files).
|
||
workspace_check() {
|
||
local dir="$1" got rgot rwant
|
||
got="$( (cd "$dir" && go list -m -f '{{if .Replace}}{{.Replace.Dir}}{{else}}{{.Dir}}{{end}}' git.golem15.com/golem15/summercms 2>/dev/null) || true)"
|
||
rgot="$(realpath -e -- "$got" 2>/dev/null || true)"
|
||
rwant="$(realpath -e -- "$ROOT" 2>/dev/null || true)"
|
||
if [[ -z "$got" || -z "$rgot" || "$rgot" != "$rwant" ]]; then
|
||
echo "refuse: $dir resolves git.golem15.com/golem15/summercms to ${got:-<nothing>}, not $ROOT" >&2
|
||
return 1
|
||
fi
|
||
echo "workspace: $dir resolves the framework to $rwant"
|
||
}
|
||
|
||
|
||
|
||
# marker_hits DIR BASE prints file:line for every line added since BASE to
|
||
# a non-test Go file, a README or a docs page under modules/ or docs/ that
|
||
# names SEO, a sitemap, JSON-LD or a consuming application (D-90).
|
||
marker_hits() {
|
||
local dir="$1" base="$2" pattern
|
||
pattern="$(IFS='|'; echo "${MARKER_WORDS[*]}")|$(IFS='|'; echo "${APP_NAMES[*]}")"
|
||
git -C "$dir" diff -U0 "$base" HEAD -- 'modules/*.go' 'modules/**/*.go' 'modules/**/README.md' 'docs' ':(exclude)*_test.go' ':(exclude)**/*_test.go' |
|
||
PATTERN="$pattern" python3 -c '
|
||
import os, re, sys
|
||
pat = re.compile(os.environ["PATTERN"], re.I)
|
||
path, line = "", 0
|
||
for raw in sys.stdin:
|
||
if raw.startswith("+++ "):
|
||
path = raw[6:].rstrip("\n") if raw.startswith("+++ b/") else ""
|
||
continue
|
||
m = re.match(r"^@@ -\S+ \+(\d+)", raw)
|
||
if m:
|
||
line = int(m.group(1))
|
||
continue
|
||
if raw.startswith("+") and not raw.startswith("+++"):
|
||
if path and pat.search(raw[1:]):
|
||
print(f"{path}:{line}: {raw[1:].strip()[:120]}")
|
||
line += 1
|
||
'
|
||
}
|
||
|
||
# npm_vitest DIR runs npm run test (vitest) in DIR and checks its output.
|
||
npm_vitest() {
|
||
local dir="$1" log rc=0 dc=0
|
||
require_script "$dir" test
|
||
log="$(mktemp)"
|
||
npm --prefix "$dir" run test >"$log" 2>&1 || rc=$?
|
||
detect_vitest "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
grep -E '(FAIL|×|Error|expected)' "$log" | head -n 40 >&2 || true
|
||
tail -n 20 "$log" >&2
|
||
rm -f "$log"
|
||
echo "refuse: npm run test in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
echo "vitest: $dir $(grep -E '^\s*Tests\s' "$log" | tr -s ' ')"
|
||
rm -f "$log"
|
||
}
|
||
|
||
# detect_tap reads a node --test --test-reporter=tap run. Exit 1 a failed
|
||
# test ("not ok", "# fail" or "# cancelled" above 0), 2 skipped or todo
|
||
# tests, 3 no all-passing summary.
|
||
detect_tap() {
|
||
python3 - "$1" <<'PY'
|
||
import re, sys
|
||
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||
text = re.sub(r"\x1b\[[0-9;]*m", "", text)
|
||
counts = {k: int(v) for k, v in re.findall(r"^# (tests|pass|fail|skipped|todo|cancelled) (\d+)\s*$", text, re.M)}
|
||
if re.search(r"^\s*not ok\b", text, re.M) or counts.get("fail", 0) > 0 or counts.get("cancelled", 0) > 0:
|
||
print("refuse: TAP reports failed tests", file=sys.stderr)
|
||
sys.exit(1)
|
||
if counts.get("skipped", 0) > 0 or counts.get("todo", 0) > 0 or re.search(r"^\s*ok .*# (SKIP|TODO)\b", text, re.M | re.I):
|
||
print("refuse: TAP reports skipped or todo tests", file=sys.stderr)
|
||
sys.exit(2)
|
||
if "fail" not in counts or counts.get("pass", 0) == 0 or counts.get("tests", -1) != counts.get("pass"):
|
||
print("refuse: no all-passing TAP summary", file=sys.stderr)
|
||
sys.exit(3)
|
||
PY
|
||
}
|
||
|
||
# node_tap DIR runs npm run test (node --test with the TAP reporter) in DIR
|
||
# and requires an all-passing TAP summary.
|
||
node_tap() {
|
||
local dir="$1" log rc=0 dc=0
|
||
require_script "$dir" test
|
||
log="$(mktemp)"
|
||
npm --prefix "$dir" run test >"$log" 2>&1 || rc=$?
|
||
detect_tap "$log" || dc=$?
|
||
if [[ "$rc" -ne 0 || "$dc" -ne 0 ]]; then
|
||
grep -E '^\s*not ok|# (fail|skipped|todo)|Error' "$log" | head -n 40 >&2 || true
|
||
tail -n 20 "$log" >&2
|
||
rm -f "$log"
|
||
echo "refuse: npm run test in $dir (test=$rc detect=$dc)" >&2
|
||
return 1
|
||
fi
|
||
echo "tap: $dir $(grep -E '^# (tests|pass|fail) ' "$log" | tr '\n' ' ')"
|
||
rm -f "$log"
|
||
}
|
||
|
||
# require_script DIR NAME refuses DIR without node_modules or without the
|
||
# npm script NAME in its package.json.
|
||
require_script() {
|
||
local dir="$1" name="$2"
|
||
require_file "$dir/package.json" "npm package"
|
||
require_dir "$dir/node_modules" "npm dependencies; run npm ci in $dir"
|
||
if ! python3 -c 'import json, sys; sys.exit(0 if sys.argv[2] in json.load(open(sys.argv[1])).get("scripts", {}) else 1)' "$dir/package.json" "$name"; then
|
||
echo "refuse: missing npm script $name in $dir/package.json" >&2
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# npm_script DIR NAME runs npm run NAME in DIR; the last lines of its output
|
||
# are printed on failure.
|
||
npm_script() {
|
||
local dir="$1" name="$2" log rc=0
|
||
require_script "$dir" "$name"
|
||
log="$(mktemp)"
|
||
npm --prefix "$dir" run "$name" >"$log" 2>&1 || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
tail -n 40 "$log" >&2
|
||
rm -f "$log"
|
||
echo "refuse: npm run $name in $dir (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
rm -f "$log"
|
||
echo "npm: $name in $dir"
|
||
}
|
||
|
||
# node_run DIR SCRIPT ARGS... runs a node script of DIR from DIR.
|
||
node_run() {
|
||
local dir="$1" script="$2" log rc=0
|
||
shift 2
|
||
require_file "$dir/$script" "node script"
|
||
log="$(mktemp)"
|
||
(cd "$dir" && node "$script" "$@") >"$log" 2>&1 || rc=$?
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
tail -n 40 "$log" >&2
|
||
rm -f "$log"
|
||
echo "refuse: node $script in $dir (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
rm -f "$log"
|
||
echo "node: $script in $dir"
|
||
}
|
||
|
||
# require_all WHAT DIR... checks every directory and names all missing ones.
|
||
require_all() {
|
||
local what="$1" dir missing=0
|
||
shift
|
||
for dir in "$@"; do
|
||
require_dir "$dir" "$what" || missing=1
|
||
done
|
||
return "$missing"
|
||
}
|
||
|
||
expect_detector() {
|
||
local detector="$1" name="$2" want="$3" payload="$4" log dc=0
|
||
log="$(mktemp)"
|
||
printf '%s\n' "$payload" >"$log"
|
||
"$detector" "$log" >/dev/null 2>&1 || dc=$?
|
||
rm -f "$log"
|
||
if [[ "$dc" -ne "$want" ]]; then
|
||
echo "refuse: self-test $detector $name: exit $dc, want $want" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test $detector $name"
|
||
}
|
||
|
||
# expect_mode NAME WANT_EXIT WANT_TEXT MODE ENV... runs this script in MODE
|
||
# with ENV and checks its exit status and output.
|
||
expect_mode() {
|
||
local name="$1" want="$2" text="$3" mode="$4"
|
||
shift 4
|
||
local out rc=0
|
||
out="$(env "$@" bash "${BASH_SOURCE[0]}" "$mode" 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne "$want" ]] || ! grep -qF -- "$text" <<<"$out"; then
|
||
printf '%s\n' "$out" | tail -n 10 >&2
|
||
echo "refuse: self-test $name: exit $rc (want $want), output must carry \"$text\"" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test $name"
|
||
}
|
||
|
||
|
||
run_self_test() {
|
||
bash -n "${BASH_SOURCE[0]}"
|
||
expect_detector detect_go pass 0 '{"Action":"pass","Package":"p","Test":"TestContentDescriptorFields"}'
|
||
expect_detector detect_go fail 1 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"fail","Package":"p","Test":"TestFieldTypeRequestOptions"}'
|
||
expect_detector detect_go build 1 '{"Action":"build-fail","ImportPath":"p"}'
|
||
expect_detector detect_go skip 2 '{"Action":"pass","Package":"p","Test":"TestA"}
|
||
{"Action":"skip","Package":"p","Test":"TestB"}'
|
||
expect_detector detect_go subtest-skip 2 '{"Action":"skip","Package":"p","Test":"TestA/sub"}
|
||
{"Action":"pass","Package":"p","Test":"TestA"}'
|
||
expect_detector detect_go zero 3 '{"Action":"pass","Package":"p"}'
|
||
expect_detector detect_go no-tests 3 '{"Action":"output","Package":"p","Output":"testing: warning: no tests to run\n"}
|
||
{"Action":"pass","Package":"p"}'
|
||
expect_detector detect_go nonjson 4 '{"Action":"pass",'
|
||
REQUIRE_TESTS="TestA TestB" expect_detector detect_go missing-named 5 '{"Action":"pass","Package":"p","Test":"TestA"}'
|
||
REQUIRE_TESTS="TestA" expect_detector detect_go subtest-is-not-parent 5 '{"Action":"pass","Package":"p","Test":"TestOuter/TestA"}
|
||
{"Action":"pass","Package":"p","Test":"TestOther"}'
|
||
local ok=' ✓ tests/a.test.ts (12 tests) 15ms
|
||
|
||
Test Files 1 passed (1)
|
||
Tests 12 passed (12)'
|
||
expect_detector detect_vitest pass 0 "$ok"
|
||
REQUIRE_FILES="tests/b.test.ts" expect_detector detect_vitest missing-file 5 "$ok"
|
||
expect_detector detect_vitest failed 1 ' Test Files 1 failed | 1 passed (2)
|
||
Tests 1 failed | 16 passed (17)'
|
||
expect_detector detect_vitest no-summary 3 ' ✓ tests/a.test.ts (12 tests) 15ms'
|
||
local tap='TAP version 13
|
||
ok 1 - a
|
||
ok 2 - b
|
||
1..2
|
||
# tests 2
|
||
# suites 0
|
||
# pass 2
|
||
# fail 0
|
||
# cancelled 0
|
||
# skipped 0
|
||
# todo 0'
|
||
expect_detector detect_tap pass 0 "$tap"
|
||
expect_detector detect_tap not-ok 1 "${tap/ok 2 - b/not ok 2 - b}"
|
||
expect_detector detect_tap fail-count 1 "${tap/\# fail 0/\# fail 1}"
|
||
expect_detector detect_tap skipped 2 "${tap/\# skipped 0/\# skipped 1}"
|
||
expect_detector detect_tap skip-directive 2 "${tap/ok 2 - b/ok 2 - b # SKIP not here}"
|
||
expect_detector detect_tap no-summary 3 'TAP version 13
|
||
ok 1 - a'
|
||
expect_detector detect_tap zero 3 "$(printf '# tests 0\n# pass 0\n# fail 0')"
|
||
|
||
local tmp
|
||
tmp="$(mktemp -d)"
|
||
|
||
# A failing go test in a planted module is refused; the passing one is
|
||
# not.
|
||
mkdir -p "$tmp/gomod"
|
||
printf 'module selftest\n\ngo 1.22\n' >"$tmp/gomod/go.mod"
|
||
printf 'package selftest\n\nimport "testing"\n\nfunc TestPlanted(t *testing.T) {}\n' >"$tmp/gomod/p_test.go"
|
||
if ! GOWORK=off GOFLAGS=-mod=mod go_json "$tmp/gomod" -count=1 ./... >/dev/null 2>&1; then
|
||
rm -rf -- "${tmp:?}"
|
||
echo "refuse: self-test go_json refused a passing planted test" >&2
|
||
return 1
|
||
fi
|
||
printf 'package selftest\n\nimport "testing"\n\nfunc TestPlanted(t *testing.T) { t.Fatal("planted") }\n' >"$tmp/gomod/p_test.go"
|
||
if GOWORK=off GOFLAGS=-mod=mod go_json "$tmp/gomod" -count=1 ./... >/dev/null 2>&1; then
|
||
rm -rf -- "${tmp:?}"
|
||
echo "refuse: self-test go_json accepted a failing planted test" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test go_json failing-test"
|
||
|
||
# The marker rule on a planted repository: an added SEO word, sitemap,
|
||
# JSON-LD or application name in a module source or a docs page is
|
||
# found; a test file, and a line that was there before the base, are not.
|
||
mkdir -p "$tmp/repo"
|
||
git -C "$tmp/repo" init -q
|
||
mkdir -p "$tmp/repo/modules/pact" "$tmp/repo/docs"
|
||
printf 'package pact\n\n// Old mentions a site''map from before the phase.\nfunc Old() {}\n' >"$tmp/repo/modules/pact/old.go"
|
||
git -C "$tmp/repo" add -A
|
||
git -C "$tmp/repo" -c user.name=t -c user.email=t@example.test commit -q -m base
|
||
local base
|
||
base="$(git -C "$tmp/repo" rev-parse HEAD)"
|
||
printf 'package pact\n\n// Content describes records.\nfunc Content() {}\n' >"$tmp/repo/modules/pact/content.go"
|
||
printf 'package pact\n\n// The site''map test.\nfunc helper() {}\n' >"$tmp/repo/modules/pact/content_test.go"
|
||
git -C "$tmp/repo" add -A
|
||
git -C "$tmp/repo" -c user.name=t -c user.email=t@example.test commit -q -m clean
|
||
if [[ -n "$(marker_hits "$tmp/repo" "$base")" ]]; then
|
||
rm -rf -- "${tmp:?}"
|
||
echo "refuse: self-test marker_hits flagged a clean change or a test file" >&2
|
||
return 1
|
||
fi
|
||
printf 'package pact\n\n// Content feeds the site''map.\nfunc Content() {}\n' >"$tmp/repo/modules/pact/content.go"
|
||
printf '# Content\n\nRendered as JSON''-LD for grzy''byfunkcjonalne.\n' >"$tmp/repo/docs/content.md"
|
||
git -C "$tmp/repo" add -A
|
||
git -C "$tmp/repo" -c user.name=t -c user.email=t@example.test commit -q -m dirty
|
||
local hits
|
||
hits="$(marker_hits "$tmp/repo" "$base")"
|
||
if ! grep -q '^modules/pact/content.go:3:' <<<"$hits" || ! grep -q '^docs/content.md:3:' <<<"$hits"; then
|
||
rm -rf -- "${tmp:?}"
|
||
echo "refuse: self-test marker_hits missed a planted name: $hits" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test marker_hits"
|
||
|
||
# A stage whose repository is missing fails naming it.
|
||
expect_mode missing-plugin 1 "missing $tmp/nope-plugin" --plugins "SITEMAP_PLUGIN=$tmp/nope-plugin"
|
||
expect_mode missing-layer 1 "missing $tmp/nope-layer" --layers "SEO_LAYER=$tmp/nope-layer"
|
||
expect_mode missing-app 1 "missing $tmp/nope-app" --consumers "BM_APP=$tmp/nope-app"
|
||
expect_mode missing-compat 1 "missing $tmp/nope-compat" --compat "DEMO_APP=$tmp/nope-compat"
|
||
|
||
# The docker NOT RUN rule: a docker that does not answer.
|
||
mkdir -p "$tmp/bin"
|
||
printf '#!/bin/sh\nexit 1\n' >"$tmp/bin/docker"
|
||
chmod +x "$tmp/bin/docker"
|
||
expect_mode docker-not-run 0 "NOT RUN docker" --docker-gate "PATH=$tmp/bin:$PATH" PHASE33_REQUIRE_DOCKER=0
|
||
expect_mode docker-required 1 "FAIL docker" --docker-gate "PATH=$tmp/bin:$PATH" PHASE33_REQUIRE_DOCKER=1
|
||
expect_mode docker-stage-needs-daemon 1 "needs a reachable Docker daemon" --docker "PATH=$tmp/bin:$PATH"
|
||
rm -rf -- "${tmp:?}"
|
||
|
||
# No argument prints the usage and exits 2.
|
||
local out rc=0
|
||
out="$(bash "${BASH_SOURCE[0]}" 2>&1)" || rc=$?
|
||
if [[ "$rc" -ne 2 ]] || ! grep -qF -- "--compat" <<<"$out"; then
|
||
echo "refuse: self-test usage: exit $rc, want 2 with the usage" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test usage"
|
||
self_test_additive
|
||
echo "phase33 self-test passed"
|
||
}
|
||
|
||
# additive_case TOOL NAME WANT BASE_SRC HEAD_SRC runs a planted package pair
|
||
# through scripts/apisurface and additive_compare; WANT is the line the
|
||
# comparison must print ("removed or changed", "method added to existing
|
||
# interface" or "ok").
|
||
additive_case() {
|
||
local tool="$1" name="$2" want="$3" dir out rc=0 ok=0
|
||
dir="$(mktemp -d)"
|
||
mkdir -p "$dir/base" "$dir/head"
|
||
printf '%s\n' "$4" >"$dir/base/p.go"
|
||
printf '%s\n' "$5" >"$dir/head/p.go"
|
||
if api_surface "$tool" "$dir/base" "$dir/base.api" && api_surface "$tool" "$dir/head" "$dir/head.api"; then
|
||
out="$(additive_compare "selftest/$name" "$dir/base.api" "$dir/head.api" 2>&1)" || rc=$?
|
||
case "$want" in
|
||
ok) [[ "$rc" -eq 0 ]] && grep -q '^additive: ok selftest/' <<<"$out" && ok=1 ;;
|
||
*) [[ "$rc" -ne 0 ]] && grep -q "^additive: $want in selftest/$name: " <<<"$out" && ok=1 ;;
|
||
esac
|
||
fi
|
||
rm -rf -- "${dir:?}"
|
||
if [[ "$ok" -ne 1 ]]; then
|
||
printf '%s\n' "${out:-}" >&2
|
||
echo "refuse: self-test additive $name (want $want, exit $rc)"
|
||
return 1
|
||
fi
|
||
echo "PASS self-test additive $name"
|
||
}
|
||
|
||
# self_test_additive plants the refused and the accepted API changes.
|
||
self_test_additive() {
|
||
local tmp tool failed=0
|
||
tmp="$(mktemp -d)"
|
||
tool="$tmp/apisurface"
|
||
(cd "$ROOT" && go build -o "$tool" ./scripts/apisurface)
|
||
mkdir -p "$tmp/empty"
|
||
local rc=0
|
||
"$tool" "$tmp/empty" >/dev/null 2>&1 || rc=$?
|
||
if [[ "$rc" -ne 2 ]]; then
|
||
rm -rf -- "${tmp:?}"
|
||
echo "refuse: self-test apisurface: exit $rc for a directory without Go files, want 2" >&2
|
||
return 1
|
||
fi
|
||
echo "PASS self-test apisurface no-go-files"
|
||
local p=$'package p\n\n'
|
||
additive_case "$tool" removed-func "removed or changed" "${p}func A() {}"$'\n\n'"func B() {}" "${p}func A() {}" || failed=1
|
||
additive_case "$tool" changed-param "removed or changed" "${p}func F(n int) {}" "${p}func F(n int64) {}" || failed=1
|
||
additive_case "$tool" multiline-param-added "removed or changed" \
|
||
"${p}"$'func F(\n\ta int,\n\tb string,\n) error {\n\treturn nil\n}' \
|
||
"${p}"$'func F(\n\ta int,\n\tb string,\n\tc bool,\n) error {\n\treturn nil\n}' || failed=1
|
||
additive_case "$tool" interface-method-added "method added to existing interface" \
|
||
"${p}"$'// HasThing is an optional capability.\ntype HasThing interface {\n\tThing() string\n}' \
|
||
"${p}"$'// HasThing is an optional capability.\ntype HasThing interface {\n\tThing() string\n\tOther() error\n}' || failed=1
|
||
additive_case "$tool" field-type-changed "removed or changed" "${p}"$'type S struct {\n\tA int\n}' "${p}"$'type S struct {\n\tA int64\n}' || failed=1
|
||
additive_case "$tool" const-value-changed "removed or changed" "${p}const C = 1" "${p}const C = 2" || failed=1
|
||
additive_case "$tool" func-added ok "${p}func A() {}" "${p}func A() {}"$'\n\n'"func B(s string) error { return nil }" || failed=1
|
||
additive_case "$tool" struct-field-added ok \
|
||
"${p}"$'type S struct {\n\tA int\n\tB string `json:"b"`\n}' \
|
||
"${p}"$'type S struct {\n\tA int\n\tB string `json:"b"`\n\tLonger bool\n}' || failed=1
|
||
additive_case "$tool" optional-interface-added ok \
|
||
"${p}"$'type HasThing interface {\n\tThing() string\n}' \
|
||
"${p}"$'type HasThing interface {\n\tThing() string\n}\n\n// HasOther is a new optional capability.\ntype HasOther interface {\n\tOther() error\n\tMore() int\n}' || failed=1
|
||
additive_case "$tool" method-added-to-struct ok \
|
||
"${p}"$'type S struct{}\n\nfunc (S) A() {}' \
|
||
"${p}"$'type S struct{}\n\nfunc (S) A() {}\n\nfunc (s *S) B(n int) error { return nil }' || failed=1
|
||
additive_case "$tool" param-renamed ok "${p}func F(a int, b string) {}" "${p}func F(count int, label string) {}" || failed=1
|
||
additive_case "$tool" comment-edited ok "${p}"$'// F does x.\nfunc F() {}' "${p}"$'// F does y,\n// in two lines.\nfunc F() {}' || failed=1
|
||
rm -rf -- "${tmp:?}"
|
||
if [[ "$failed" -ne 0 ]]; then
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
|
||
run_go() {
|
||
go_vet "$ROOT" ./...
|
||
go_short "$ROOT"
|
||
go_pkg_named "$ROOT" "-short" "${GO_TESTS[@]}"
|
||
echo "phase33 go passed"
|
||
}
|
||
|
||
run_plugins() {
|
||
local dir
|
||
require_all "Go plugin" "$SITEMAP_PLUGIN" "$SEO_PLUGIN" "$JOURNAL_PLUGIN"
|
||
for dir in "$SITEMAP_PLUGIN" "$SEO_PLUGIN" "$JOURNAL_PLUGIN"; do
|
||
workspace_check "$dir"
|
||
go_vet "$dir" ./...
|
||
go_short "$dir"
|
||
done
|
||
go_pkg_named "$SITEMAP_PLUGIN" "-short" "${SITEMAP_UNIT_TESTS[@]}"
|
||
go_pkg_named "$SEO_PLUGIN" "-short" "${SEO_UNIT_TESTS[@]}"
|
||
go_pkg_named "$JOURNAL_PLUGIN" "-short" "${JOURNAL_UNIT_TESTS[@]}"
|
||
npm_vitest "$SEO_PLUGIN"
|
||
npm_vitest "$JOURNAL_PLUGIN"
|
||
echo "phase33 plugins passed"
|
||
}
|
||
|
||
run_layers() {
|
||
local layer
|
||
require_all "Nuxt layer" "$SEO_LAYER" "$JOURNAL_LAYER" "$MENU_LAYER" "$FAQ_LAYER"
|
||
for layer in "$SEO_LAYER" "$JOURNAL_LAYER" "$MENU_LAYER" "$FAQ_LAYER"; do
|
||
node_tap "$layer"
|
||
npm_script "$layer" build:playground
|
||
done
|
||
# vue-seo-layer: every smoke expectation (the layer on, off, the CMS
|
||
# down, without the 404 token, with drift), the build-guard fixtures
|
||
# and the purge checks.
|
||
npm_script "$SEO_LAYER" smoke:ssr
|
||
npm_script "$SEO_LAYER" guard:check
|
||
npm_script "$SEO_LAYER" purge:check
|
||
# vue-journal-layer: the acme expectations, the post title forced to h1
|
||
# and to p (postTitleTag), the link hint and the breadcrumbs off, the
|
||
# purge checks and the git-install lockfile reading.
|
||
local journal_smoke=(testing/smoke-ssr.mjs --output .playground/.output --fixtures testing/fixtures/acme)
|
||
node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme.txt
|
||
node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-title-h1.txt --env NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG=h1
|
||
node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-title-p.txt --env NUXT_PUBLIC_JOURNAL_POST_TITLE_TAG=p
|
||
node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-hint-off.txt --env NUXT_PUBLIC_SEO_LINKS_NEW_TAB_HINT=false
|
||
node_run "$JOURNAL_LAYER" "${journal_smoke[@]}" --expect testing/expect-acme-breadcrumbs-off.txt --env NUXT_PUBLIC_JOURNAL_BREADCRUMBS=false
|
||
node_run "$JOURNAL_LAYER" testing/purge-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --kind post --slug hello-acme --path /blog/hello-acme
|
||
node_run "$JOURNAL_LAYER" testing/purge-token-check.mjs --output .playground/.output --fixtures testing/fixtures/acme --path /blog/hello-acme
|
||
(cd "$JOURNAL_LAYER" && bash scripts/check-git-install.sh --self-test)
|
||
npm_script "$MENU_LAYER" smoke:ssr
|
||
npm_script "$FAQ_LAYER" smoke:ssr
|
||
echo "phase33 layers passed"
|
||
}
|
||
|
||
run_consumers() {
|
||
local app pkg
|
||
require_all "application" "$DEMO_APP" "$GRZYBY_APP" "$SUMMERCMSIO_APP" "$BM_APP"
|
||
for app in "$DEMO_APP" "$GRZYBY_APP" "$SUMMERCMSIO_APP" "$BM_APP"; do
|
||
workspace_check "$app"
|
||
(cd "$app" && go build ./...)
|
||
echo "build: $app"
|
||
for pkg in "$app"/vue-*/package.json; do
|
||
[[ -f "$pkg" ]] || continue
|
||
npm_script "$(dirname "$pkg")" build
|
||
done
|
||
done
|
||
# The app patches of plans 33-12, 33-13 and 33-14 apply to the apps as
|
||
# committed; 33-14's demo locale patches apply on top of the 33-12 demo
|
||
# patches (checked in scratch worktrees).
|
||
patch_check "$DEMO_APP" 33-12-sm-demo-app.patch
|
||
patch_check "$DEMO_APP/vue-demo-app" 33-12-vue-demo-app.patch
|
||
patch_check "$SUMMERCMSIO_APP" 33-12-sm-summercmsio-app.patch
|
||
patch_check "$SUMMERCMSIO_APP/vue-summercmsio-app" 33-12-vue-summercmsio-app.patch
|
||
patch_check "$GRZYBY_APP" 33-13-sm-grzybyfunkcjonalne-app.patch
|
||
patch_check "$GRZYBY_APP/vue-grzybyfunkcjonalne-app" 33-13-vue-grzybyfunkcjonalne-app.patch
|
||
scratch_init
|
||
stacked_patch_check "$DEMO_APP/vue-demo-app" 33-12-vue-demo-app.patch 33-14-vue-demo-app-locale.patch
|
||
if [[ -f "$PHASE_DIR/33-14-sm-demo-app-locale.patch" ]]; then
|
||
stacked_patch_check "$DEMO_APP" 33-12-sm-demo-app.patch 33-14-sm-demo-app-locale.patch
|
||
fi
|
||
echo "phase33 consumers passed"
|
||
}
|
||
|
||
# PHASE_DIR holds the app patches.
|
||
PHASE_DIR="$ROOT/.planning/phases/33-seo-output-audit-and-content"
|
||
|
||
# patch_check REPO PATCH runs git apply --check of a phase patch in REPO
|
||
# (--allow-empty: an empty patch, such as summercms.io's Go side, passes).
|
||
patch_check() {
|
||
local repo="$1" patch="$PHASE_DIR/$2"
|
||
require_dir "$repo" "patch target"
|
||
require_file "$patch" "app patch"
|
||
if ! git -C "$repo" apply --check --allow-empty "$patch"; then
|
||
echo "refuse: $2 does not apply to $repo" >&2
|
||
return 1
|
||
fi
|
||
echo "patch: $2 applies to $repo"
|
||
}
|
||
|
||
# stacked_patch_check REPO BASE PATCH applies BASE in a scratch worktree of
|
||
# REPO's HEAD and runs git apply --check of PATCH on top of it.
|
||
stacked_patch_check() {
|
||
local repo="$1" base="$PHASE_DIR/$2" patch="$PHASE_DIR/$3" wt
|
||
require_file "$base" "app patch"
|
||
require_file "$patch" "app patch"
|
||
wt="$SCRATCH/stack-$(basename "$repo")-$RANDOM"
|
||
add_worktree "$repo" "$wt" HEAD
|
||
if ! git -C "$wt" apply --allow-empty "$base" || ! git -C "$wt" apply --check --allow-empty "$patch"; then
|
||
echo "refuse: $3 does not apply to $repo with $2" >&2
|
||
return 1
|
||
fi
|
||
echo "patch: $3 applies to $repo on top of $2"
|
||
}
|
||
|
||
docker_up() {
|
||
command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1
|
||
}
|
||
|
||
run_docker() {
|
||
local dir
|
||
if ! docker_up; then
|
||
echo "refuse: the docker stage needs a reachable Docker daemon (testcontainers); run it on a Docker host" >&2
|
||
return 1
|
||
fi
|
||
require_all "Go plugin" "$SITEMAP_PLUGIN" "$SEO_PLUGIN" "$JOURNAL_PLUGIN"
|
||
local seo_required=("${SEO_DOCKER_TESTS[@]}") seo_skip=()
|
||
if chrome_binary >/dev/null; then
|
||
seo_required+=(TestChromeRenderer)
|
||
echo "chrome: $(chrome_binary) (TestChromeRenderer required)"
|
||
else
|
||
seo_skip=(-skip '^TestChromeRenderer$')
|
||
echo "NOT RUN TestChromeRenderer: no Chrome or Chromium on this host (set SEO_TEST_CHROME)"
|
||
fi
|
||
workspace_check "$SITEMAP_PLUGIN"
|
||
REQUIRE_TESTS="${SITEMAP_DOCKER_TESTS[*]}" go_json "$SITEMAP_PLUGIN" -count=1 -timeout 30m ./...
|
||
workspace_check "$SEO_PLUGIN"
|
||
REQUIRE_TESTS="${seo_required[*]}" go_json "$SEO_PLUGIN" -count=1 -timeout 30m "${seo_skip[@]}" ./...
|
||
workspace_check "$JOURNAL_PLUGIN"
|
||
REQUIRE_TESTS="${JOURNAL_DOCKER_TESTS[*]}" go_json "$JOURNAL_PLUGIN" -count=1 -timeout 30m ./...
|
||
# The framework's attachment and WebP packages with their Postgres
|
||
# suites (33-11).
|
||
go_json "$ROOT" -count=1 -timeout 30m ./modules/lagoon/attach/...
|
||
echo "phase33 docker passed"
|
||
}
|
||
|
||
# chrome_binary prints the Chrome or Chromium binary TestChromeRenderer
|
||
# uses (SEO_TEST_CHROME, else chromium, chromium-browser, google-chrome on
|
||
# PATH) and fails when there is none.
|
||
chrome_binary() {
|
||
if [[ -n "${SEO_TEST_CHROME:-}" ]]; then
|
||
printf '%s\n' "$SEO_TEST_CHROME"
|
||
return 0
|
||
fi
|
||
local name
|
||
for name in chromium chromium-browser google-chrome; do
|
||
if command -v "$name" >/dev/null 2>&1; then
|
||
command -v "$name"
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# docker_gate is the docker step of --all: run the stage when a daemon
|
||
# answers, fail with PHASE33_REQUIRE_DOCKER=1, print NOT RUN otherwise.
|
||
docker_gate() {
|
||
if docker_up; then
|
||
if bash "${BASH_SOURCE[0]}" --docker; then
|
||
echo "PASS docker"
|
||
return 0
|
||
fi
|
||
echo "FAIL docker"
|
||
return 1
|
||
fi
|
||
if [[ "${PHASE33_REQUIRE_DOCKER:-0}" == "1" ]]; then
|
||
echo "FAIL docker: no reachable Docker daemon and PHASE33_REQUIRE_DOCKER=1"
|
||
return 1
|
||
fi
|
||
cat >&2 <<'EOF'
|
||
************************************************************************
|
||
NOT RUN docker: no reachable Docker daemon on this host.
|
||
The Postgres suites of the sitemap, SEO and Journal plugins did NOT run.
|
||
Run on a Docker host (through sudo -u golem when the socket is denied)
|
||
before /msd-verify-work:
|
||
bash scripts/check-phase33.sh --docker
|
||
************************************************************************
|
||
EOF
|
||
echo "NOT RUN docker (Docker-host only; see the notice above)"
|
||
}
|
||
|
||
run_admin() {
|
||
(cd "$ROOT" && bash scripts/check-admin-openapi.sh --check)
|
||
(cd "$ROOT" && bash scripts/check-admin-dist.sh)
|
||
echo "phase33 admin passed"
|
||
}
|
||
|
||
run_docs() {
|
||
go_named "$ROOT" ./cmd/summer "" TestDocsTree
|
||
(cd "$ROOT" && go run ./cmd/summer docs:build --check)
|
||
echo "phase33 docs passed"
|
||
}
|
||
|
||
run_marker() {
|
||
if ! git -C "$ROOT" rev-parse --verify -q "$PHASE33_BASE^{commit}" >/dev/null; then
|
||
echo "refuse: PHASE33_BASE $PHASE33_BASE is not a commit of $ROOT" >&2
|
||
return 1
|
||
fi
|
||
local hits
|
||
hits="$(marker_hits "$ROOT" "$PHASE33_BASE")"
|
||
if [[ -n "$hits" ]]; then
|
||
printf '%s\n' "$hits" | head -n 40 >&2
|
||
echo "refuse: framework lines added since $PHASE33_BASE name SEO, a sitemap, JSON-LD or a consuming application (D-90)" >&2
|
||
return 1
|
||
fi
|
||
echo "marker: no line added since $PHASE33_BASE under modules/ or docs/ names SEO, a sitemap, JSON-LD or a consuming application"
|
||
echo "phase33 marker passed"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# additive and compat (D-106: the release is a patch tag, so the
|
||
# framework change must be strictly additive and existing consumers must
|
||
# build unchanged).
|
||
|
||
# scratch_init creates the stage's scratch directory and the exit trap that
|
||
# removes it and every worktree registered with add_worktree.
|
||
SCRATCH=""
|
||
CLEANUP_WORKTREES=()
|
||
scratch_cleanup() {
|
||
local entry
|
||
for entry in "${CLEANUP_WORKTREES[@]}"; do
|
||
git -c safe.directory='*' -C "${entry%%|*}" worktree remove --force "${entry#*|}" >/dev/null 2>&1 || true
|
||
git -c safe.directory='*' -C "${entry%%|*}" worktree prune >/dev/null 2>&1 || true
|
||
done
|
||
if [[ -n "$SCRATCH" && -d "$SCRATCH" ]]; then
|
||
chmod -R u+w -- "${SCRATCH:?}" 2>/dev/null || true
|
||
rm -rf -- "${SCRATCH:?}"
|
||
fi
|
||
}
|
||
scratch_init() {
|
||
SCRATCH="$(mktemp -d)"
|
||
trap scratch_cleanup EXIT
|
||
}
|
||
|
||
# add_worktree REPO PATH REV adds a detached worktree (git worktree add
|
||
# --detach) that the exit trap removes again.
|
||
add_worktree() {
|
||
git -c safe.directory='*' -C "$1" worktree add --detach -q "$2" "$3"
|
||
CLEANUP_WORKTREES+=("$1|$2")
|
||
}
|
||
|
||
# snapshot GITDIR REV DEST writes the tree of REV into DEST through a
|
||
# temporary index: nothing is written to the repository, so read-only
|
||
# checkouts (other users' repositories, submodule git dirs) work too.
|
||
snapshot() {
|
||
local gitdir="$1" rev="$2" dest="$3" index rc=0
|
||
index="${SCRATCH:?}/index.$RANDOM$RANDOM"
|
||
mkdir -p "$dest"
|
||
GIT_INDEX_FILE="$index" git -c safe.directory='*' --git-dir="$gitdir" read-tree "$rev" >/dev/null 2>&1 &&
|
||
GIT_INDEX_FILE="$index" git -c safe.directory='*' --git-dir="$gitdir" --work-tree="$dest" checkout-index -a -f >/dev/null 2>&1 || rc=$?
|
||
rm -f -- "$index" "$index.lock"
|
||
return "$rc"
|
||
}
|
||
|
||
# git_dir_of DIR prints the absolute git dir of the checkout in DIR.
|
||
git_dir_of() {
|
||
git -c safe.directory='*' -C "$1" rev-parse --absolute-git-dir 2>/dev/null
|
||
}
|
||
|
||
# snapshot_submodules REPO REV DEST fills every gitlink of REV under DEST
|
||
# with a snapshot of its pinned commit, read from REPO's submodule checkout
|
||
# (or REPO/.git/modules/<path>). A gitlink without a local copy of the
|
||
# pinned commit is reported and left empty; the build then shows it.
|
||
snapshot_submodules() {
|
||
local repo="$1" rev="$2" dest="$3" mode type sha path gd
|
||
while read -r mode type sha path; do
|
||
[[ "$mode" == "160000" ]] || continue
|
||
gd=""
|
||
if [[ -e "$repo/$path/.git" ]]; then
|
||
gd="$(git_dir_of "$repo/$path" || true)"
|
||
elif [[ -d "$repo/.git/modules/$path" ]]; then
|
||
gd="$repo/.git/modules/$path"
|
||
fi
|
||
if [[ -z "$gd" ]] || ! snapshot "$gd" "$sha" "$dest/$path"; then
|
||
echo "compat: note $(basename "$repo"): submodule $path at ${sha:0:12} is not available locally" >&2
|
||
fi
|
||
done < <(git -c safe.directory='*' -C "$repo" ls-tree -r "$rev")
|
||
}
|
||
|
||
# sibling_farm DIR SKIP creates DIR with a symlink to every sibling of this
|
||
# checkout except SKIP (the consumer placed there), so relative references
|
||
# of the consumer (go.mod and go.work replaces, tests reading
|
||
# ../summercms.go) resolve as they do next to the real checkouts.
|
||
sibling_farm() {
|
||
local farm="$1" skip="$2" parent entry
|
||
parent="$(dirname "$ROOT")"
|
||
mkdir -p "$farm"
|
||
for entry in "$parent"/*; do
|
||
[[ "$(basename "$entry")" == "$skip" ]] && continue
|
||
ln -s "$entry" "$farm/$(basename "$entry")"
|
||
done
|
||
}
|
||
|
||
# scratch_gowork APP FRAMEWORK prints a go.work for APP (a checkout placed in
|
||
# a sibling farm): APP's own go.work (or "use ." when it has none) with every
|
||
# use and replace path made absolute, any replace of the framework dropped,
|
||
# a use of this checkout pointed at FRAMEWORK, and the framework replaced by
|
||
# FRAMEWORK.
|
||
scratch_gowork() {
|
||
python3 - "$1" "$2" "$ROOT" <<'PY'
|
||
import os, re, sys
|
||
app, framework, root = sys.argv[1:4]
|
||
module = "git.golem15.com/golem15/summercms"
|
||
real_root = os.path.realpath(root)
|
||
used = False
|
||
|
||
def absolute(path):
|
||
global used
|
||
full = path if path.startswith("/") else os.path.normpath(os.path.join(app, path))
|
||
if os.path.realpath(full) == real_root:
|
||
used = True
|
||
return framework
|
||
return full
|
||
|
||
def replace(spec):
|
||
left, right = (part.strip() for part in spec.split("=>", 1))
|
||
if left.split()[0] == module:
|
||
return None
|
||
target = right.split()
|
||
if len(target) == 1 and target[0].startswith((".", "/")):
|
||
right = absolute(target[0])
|
||
return f"{left} => {right}"
|
||
|
||
src = os.path.join(app, "go.work")
|
||
if os.path.exists(src):
|
||
text = open(src, encoding="utf-8").read()
|
||
else:
|
||
gover = "1.27.0"
|
||
for line in open(os.path.join(app, "go.mod"), encoding="utf-8"):
|
||
m = re.match(r"^go\s+(\S+)", line)
|
||
if m:
|
||
gover = m.group(1)
|
||
break
|
||
text = f"go {gover}\n\nuse .\n"
|
||
out, block = [], None
|
||
for line in text.splitlines():
|
||
code = line.split("//", 1)[0].strip()
|
||
if block is not None:
|
||
if code == ")":
|
||
block = None
|
||
out.append(")")
|
||
elif code and block == "use":
|
||
out.append("\t" + absolute(code))
|
||
elif code:
|
||
rewritten = replace(code)
|
||
if rewritten is not None:
|
||
out.append("\t" + rewritten)
|
||
continue
|
||
if code in ("use (", "replace ("):
|
||
block = code.split()[0]
|
||
out.append(code)
|
||
continue
|
||
m = re.fullmatch(r"use\s+(\S+)", code)
|
||
if m:
|
||
out.append("use " + absolute(m.group(1)))
|
||
continue
|
||
m = re.fullmatch(r"replace\s+(.+)", code)
|
||
if m:
|
||
rewritten = replace(m.group(1))
|
||
if rewritten is not None:
|
||
out.append("replace " + rewritten)
|
||
continue
|
||
out.append(line)
|
||
if not used:
|
||
out += ["", f"replace {module} => {framework}"]
|
||
print("\n".join(out))
|
||
PY
|
||
}
|
||
|
||
# consumer_run NAME APP FRAMEWORK CMD... writes APP's scratch go.work outside
|
||
# APP, checks that the framework resolves to FRAMEWORK and runs CMD in APP
|
||
# with that workspace. The output goes to a log whose last lines are printed
|
||
# on failure unless COMPAT_QUIET=1.
|
||
consumer_run() {
|
||
local name="$1" app="$2" framework="$3" work got rc=0
|
||
shift 3
|
||
work="${SCRATCH:?}/work/$name.$(basename "$framework")"
|
||
mkdir -p "$work"
|
||
if [[ ! -f "$work/go.work" ]]; then
|
||
scratch_gowork "$app" "$framework" >"$work/go.work"
|
||
fi
|
||
got="$( (cd "$app" && GOWORK="$work/go.work" go list -m -f '{{if .Replace}}{{.Replace.Dir}}{{else}}{{.Dir}}{{end}}' git.golem15.com/golem15/summercms 2>/dev/null) || true)"
|
||
if [[ -z "$got" || "$(realpath -e -- "$got" 2>/dev/null || true)" != "$(realpath -e -- "$framework")" ]]; then
|
||
echo "refuse: compat: $name resolves git.golem15.com/golem15/summercms to ${got:-<nothing>}, not $framework" >&2
|
||
return 1
|
||
fi
|
||
(cd "$app" && GOWORK="$work/go.work" "$@") >"$work/log" 2>&1 || rc=$?
|
||
if [[ "$rc" -ne 0 && "${COMPAT_QUIET:-0}" != "1" ]]; then
|
||
grep -v '^ok ' "$work/log" | tail -n 40 >&2 || true
|
||
echo "refuse: compat: $name: $* (exit $rc)" >&2
|
||
fi
|
||
return "$rc"
|
||
}
|
||
|
||
# last_tag prints the newest release tag that does not contain HEAD: the
|
||
# previous release, also once HEAD itself carries the new release tag (a
|
||
# post-release run must not compare the release with itself).
|
||
last_tag() {
|
||
git -C "$ROOT" tag --sort=-v:refname --no-contains HEAD | head -n 1
|
||
}
|
||
|
||
# base_worktree creates the last tag's worktree once (BASE_TREE).
|
||
BASE_TREE=""
|
||
base_worktree() {
|
||
if [[ -z "$BASE_TREE" ]]; then
|
||
BASE_TREE="${SCRATCH:?}/base"
|
||
add_worktree "$ROOT" "$BASE_TREE" "$(last_tag)"
|
||
fi
|
||
}
|
||
|
||
# additive_compare PKG BASE_API HEAD_API refuses every API line of the base
|
||
# missing from HEAD (removed or changed) and every interface method or embed
|
||
# HEAD adds to an interface that exists in the base; it prints
|
||
# "additive: ok PKG (+N lines)" otherwise.
|
||
additive_compare() {
|
||
local pkg="$1" base="$2" head="$3" line name refused=0 added
|
||
while IFS= read -r line; do
|
||
[[ -n "$line" ]] || continue
|
||
echo "additive: removed or changed in $pkg: $line" >&2
|
||
refused=1
|
||
done < <(LC_ALL=C comm -23 "$base" "$head")
|
||
while IFS= read -r line; do
|
||
[[ "$line" =~ ^(imethod|iembed)\ ([A-Za-z0-9_]+)\. ]] || continue
|
||
name="${BASH_REMATCH[2]}"
|
||
if grep -qE "^type ${name}(\[.*\])? interface\$" "$base"; then
|
||
echo "additive: method added to existing interface in $pkg: $line" >&2
|
||
refused=1
|
||
fi
|
||
done < <(LC_ALL=C comm -13 "$base" "$head")
|
||
if [[ "$refused" -ne 0 ]]; then
|
||
return 1
|
||
fi
|
||
added="$(LC_ALL=C comm -13 "$base" "$head" | grep -c . || true)"
|
||
echo "additive: ok $pkg (+$added lines)"
|
||
}
|
||
|
||
# api_surface TOOL DIR OUT writes DIR's sorted API to OUT; a directory that
|
||
# does not exist or has no Go files has an empty API.
|
||
api_surface() {
|
||
local tool="$1" dir="$2" out="$3" rc=0
|
||
if [[ ! -d "$dir" ]]; then
|
||
: >"$out"
|
||
return 0
|
||
fi
|
||
"$tool" "$dir" >"$out.raw" 2>"$out.err" || rc=$?
|
||
if [[ "$rc" -eq 2 ]]; then
|
||
: >"$out.raw"
|
||
elif [[ "$rc" -ne 0 ]]; then
|
||
cat "$out.err" >&2
|
||
echo "refuse: apisurface $dir (exit $rc)" >&2
|
||
return 1
|
||
fi
|
||
LC_ALL=C sort "$out.raw" >"$out"
|
||
}
|
||
|
||
# additive_packages TAG BASE prints modules/pact, modules/cabana,
|
||
# modules/signpost and every other directory under modules/ holding a
|
||
# non-test .go file changed since TAG (uncommitted changes included) that
|
||
# exists at TAG.
|
||
additive_packages() {
|
||
local tag="$1" base="$2" dir
|
||
{
|
||
printf '%s\n' modules/pact modules/cabana modules/signpost
|
||
git -C "$ROOT" diff --name-only "$tag" -- modules |
|
||
grep -E '\.go$' | grep -vE '_test\.go$' | xargs -r -n1 dirname
|
||
} | LC_ALL=C sort -u | while IFS= read -r dir; do
|
||
if [[ -d "$base/$dir" ]]; then
|
||
echo "$dir"
|
||
fi
|
||
done
|
||
}
|
||
|
||
run_additive() {
|
||
local tag dir name failed=0 pkgs=()
|
||
tag="$(last_tag)"
|
||
if [[ -z "$tag" ]]; then
|
||
echo "refuse: additive: $ROOT has no release tag" >&2
|
||
return 1
|
||
fi
|
||
scratch_init
|
||
base_worktree
|
||
(cd "$ROOT" && go build -o "$SCRATCH/apisurface" ./scripts/apisurface)
|
||
mapfile -t pkgs < <(additive_packages "$tag" "$BASE_TREE")
|
||
mkdir -p "$SCRATCH/api"
|
||
for dir in "${pkgs[@]}"; do
|
||
name="${dir//\//_}"
|
||
api_surface "$SCRATCH/apisurface" "$BASE_TREE/$dir" "$SCRATCH/api/$name.base"
|
||
api_surface "$SCRATCH/apisurface" "$ROOT/$dir" "$SCRATCH/api/$name.head"
|
||
additive_compare "$dir" "$SCRATCH/api/$name.base" "$SCRATCH/api/$name.head" || failed=1
|
||
done
|
||
if [[ "$failed" -ne 0 ]]; then
|
||
echo "refuse: additive: the framework API changed incompatibly since $tag (D-106: a patch release is strictly additive)" >&2
|
||
return 1
|
||
fi
|
||
echo "phase33 additive passed (${#pkgs[@]} packages against $tag)"
|
||
}
|
||
|
||
# compat_tested NAME APP FRAMEWORK builds APP (also with each build tag in
|
||
# COMPAT_TAGS) and runs its short tests.
|
||
compat_tested() {
|
||
local name="$1" app="$2" framework="$3" tags
|
||
consumer_run "$name" "$app" "$framework" go build ./...
|
||
for tags in ${COMPAT_TAGS:-}; do
|
||
consumer_run "$name" "$app" "$framework" go build -tags "$tags" ./...
|
||
done
|
||
if [[ -n "${PHASE33_SKIP_TESTS:-}" ]]; then
|
||
echo "compat: note $name: tests matching $PHASE33_SKIP_TESTS skipped (PHASE33_SKIP_TESTS)" >&2
|
||
consumer_run "$name" "$app" "$framework" go test -count=1 -short -skip "$PHASE33_SKIP_TESTS" ./...
|
||
else
|
||
consumer_run "$name" "$app" "$framework" go test -count=1 -short ./...
|
||
fi
|
||
echo "compat: ok $name"
|
||
}
|
||
|
||
|
||
# compat_snapshot NAME REPO REV FARM places a read-only snapshot of REPO at
|
||
# REV, with its pinned submodules, in a sibling farm and prints its path.
|
||
compat_snapshot() {
|
||
local name="$1" repo="$2" rev="$3" farm="$4" gd
|
||
gd="$(git_dir_of "$repo" || true)"
|
||
if [[ -z "$gd" ]] || ! git -c safe.directory='*' --git-dir="$gd" rev-parse --verify -q "$rev^{commit}" >/dev/null; then
|
||
echo "refuse: compat: $repo has no revision $rev" >&2
|
||
return 1
|
||
fi
|
||
sibling_farm "$farm" "$name"
|
||
snapshot "$gd" "$rev" "$farm/$name"
|
||
snapshot_submodules "$repo" "$rev" "$farm/$name"
|
||
}
|
||
|
||
run_compat() {
|
||
local farm repo name gd real failed=0 tag skip=()
|
||
tag="$(last_tag)"
|
||
require_all "compat consumer" "$SITEMAP_PLUGIN" "$JOURNAL_PLUGIN" "$DEMO_APP"
|
||
export GOPRIVATE="${GOPRIVATE:-git.golem15.com/*}"
|
||
scratch_init
|
||
|
||
# (a) sm-sitemap-plugin and (b) sm-journal-plugin at their releases
|
||
# before the phase.
|
||
name="$(basename "$SITEMAP_PLUGIN")"
|
||
compat_snapshot "$name" "$SITEMAP_PLUGIN" "$PHASE33_SITEMAP_BASE" "$SCRATCH/a"
|
||
compat_tested "$name" "$SCRATCH/a/$name" "$ROOT"
|
||
name="$(basename "$JOURNAL_PLUGIN")"
|
||
compat_snapshot "$name" "$JOURNAL_PLUGIN" "$PHASE33_JOURNAL_BASE" "$SCRATCH/b"
|
||
compat_tested "$name" "$SCRATCH/b/$name" "$ROOT"
|
||
|
||
# (c) sm-demo-app at its commit before the phase, with the submodules
|
||
# pinned there.
|
||
name="$(basename "$DEMO_APP")"
|
||
compat_snapshot "$name" "$DEMO_APP" "$PHASE33_DEMO_BASE" "$SCRATCH/c"
|
||
COMPAT_TAGS="$MCP_TAG" compat_tested "$name" "$SCRATCH/c/$name" "$ROOT"
|
||
|
||
# (d) every other sibling that requires the framework, at HEAD with its
|
||
# pinned submodules: it builds, or it fails at the last tag too.
|
||
for repo in "$ROOT" "$SITEMAP_PLUGIN" "$JOURNAL_PLUGIN" "$DEMO_APP"; do
|
||
skip+=("$(realpath -m -- "$repo")")
|
||
done
|
||
for repo in "$PARENT"/*; do
|
||
[[ -f "$repo/go.mod" ]] || continue
|
||
grep -qE '^[[:space:]]*(require[[:space:]]+)?git\.golem15\.com/golem15/summercms[[:space:]]+v' "$repo/go.mod" || continue
|
||
real="$(realpath -m -- "$repo")"
|
||
if printf '%s\n' "${skip[@]}" | grep -qxF -- "$real"; then
|
||
continue
|
||
fi
|
||
name="$(basename "$repo")"
|
||
gd="$(git_dir_of "$repo" || true)"
|
||
if [[ -z "$gd" ]]; then
|
||
echo "compat: skipped $name (not a git checkout)"
|
||
continue
|
||
fi
|
||
farm="$SCRATCH/d/$name"
|
||
sibling_farm "$farm" "$name"
|
||
if ! snapshot "$gd" HEAD "$farm/$name"; then
|
||
echo "refuse: compat: cannot read $name at HEAD" >&2
|
||
failed=1
|
||
continue
|
||
fi
|
||
snapshot_submodules "$repo" HEAD "$farm/$name"
|
||
if COMPAT_QUIET=1 consumer_run "$name" "$farm/$name" "$ROOT" go build ./...; then
|
||
echo "compat: ok $name (build)"
|
||
continue
|
||
fi
|
||
base_worktree
|
||
if COMPAT_QUIET=1 consumer_run "$name" "$farm/$name" "$BASE_TREE" go build ./...; then
|
||
grep -v '^ok ' "$SCRATCH/work/$name.$(basename "$ROOT")/log" | tail -n 40 >&2 || true
|
||
echo "refuse: compat: $name builds at $tag but not at HEAD" >&2
|
||
failed=1
|
||
else
|
||
echo "compat: skipped $name (fails at $tag too)"
|
||
fi
|
||
done
|
||
if [[ "$failed" -ne 0 ]]; then
|
||
return 1
|
||
fi
|
||
echo "phase33 compat passed"
|
||
}
|
||
|
||
run_all() {
|
||
local stage
|
||
for stage in self-test go plugins layers consumers admin docs marker; do
|
||
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||
echo "PASS $stage"
|
||
else
|
||
echo "FAIL $stage"
|
||
exit 1
|
||
fi
|
||
done
|
||
docker_gate || exit 1
|
||
for stage in additive compat; do
|
||
if bash "${BASH_SOURCE[0]}" "--$stage"; then
|
||
echo "PASS $stage"
|
||
else
|
||
echo "FAIL $stage"
|
||
exit 1
|
||
fi
|
||
done
|
||
echo "Phase 33 gate passed"
|
||
}
|
||
|
||
run_stage() {
|
||
case "$1" in
|
||
--self-test | self-test) run_self_test ;;
|
||
--go | go) run_go ;;
|
||
--plugins | plugins) run_plugins ;;
|
||
--layers | layers) run_layers ;;
|
||
--consumers | consumers) run_consumers ;;
|
||
--docker | docker) run_docker ;;
|
||
--docker-gate) docker_gate ;;
|
||
--admin | admin) run_admin ;;
|
||
--docs | docs) run_docs ;;
|
||
--marker | marker) run_marker ;;
|
||
--additive | additive) run_additive ;;
|
||
--compat | compat) run_compat ;;
|
||
--all | all) run_all ;;
|
||
*) usage ;;
|
||
esac
|
||
}
|
||
|
||
if [[ "$#" -eq 0 ]]; then
|
||
usage
|
||
fi
|
||
if [[ "$#" -eq 1 ]]; then
|
||
run_stage "$1"
|
||
exit 0
|
||
fi
|
||
for arg in "$@"; do
|
||
if bash "${BASH_SOURCE[0]}" "$arg"; then
|
||
echo "PASS ${arg#--}"
|
||
else
|
||
echo "FAIL ${arg#--}"
|
||
exit 1
|
||
fi
|
||
done
|