feat: update
This commit is contained in:
@@ -120,11 +120,15 @@ def check_version_output(report: Report, project: Path) -> None:
|
||||
out_lines = [ln.lstrip()[5:] for ln in result.stdout.splitlines() if ln.lstrip().startswith("echo ")]
|
||||
out = "\n".join(out_lines).strip()
|
||||
single = len(out.splitlines()) == 1 and out != ""
|
||||
no_v = single and not out.startswith("v")
|
||||
detail = f"stdout={out!r}"
|
||||
if single and not no_v:
|
||||
detail += "\ncanonical version must not start with 'v'"
|
||||
report.add(
|
||||
PASS if single else FAIL,
|
||||
PASS if no_v else FAIL,
|
||||
3,
|
||||
"version 输出一行非空版本号",
|
||||
f"stdout={out!r}",
|
||||
"version 输出一行非空规范版本(无 v 前缀)",
|
||||
detail,
|
||||
)
|
||||
|
||||
|
||||
@@ -321,7 +325,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
check_build_has_no_upload(report, project)
|
||||
else:
|
||||
report.add(SKIP, 2, "ARCH 守卫与缺省值", "(build target missing)")
|
||||
report.add(SKIP, 3, "version 输出一行非空版本号", "(version target missing)")
|
||||
report.add(SKIP, 3, "version 输出一行非空规范版本(无 v 前缀)", "(version target missing)")
|
||||
report.add(SKIP, 4, "build 不含上传动作", "(build target missing)")
|
||||
|
||||
if deb_project:
|
||||
|
||||
@@ -16,7 +16,7 @@ usage() {
|
||||
# Environment:
|
||||
# DOCKER_REGISTRY Required (or --registry)
|
||||
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
|
||||
# IMAGE_TAG Optional, default: git describe --tags --always --dirty (or --tag)
|
||||
# IMAGE_TAG Optional, default: version.sh --docker (or --tag)
|
||||
# PLATFORMS Optional, default: linux/amd64 (or --platform)
|
||||
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
|
||||
# DOCKER_CONTEXT Optional, default: . (--context)
|
||||
@@ -89,19 +89,37 @@ if [[ -z "$repository" || "$repository" == /* || "$repository" == */ || "$reposi
|
||||
echo "Error: repository must be namespace/name without leading or trailing slash: $repository" >&2
|
||||
exit 2
|
||||
fi
|
||||
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
version_sh=$script_dir/version.sh
|
||||
|
||||
if [[ -z "$tag" ]]; then
|
||||
if [[ -n "$project_root" ]]; then
|
||||
tag=$(git -C "$project_root" describe --tags --always --dirty 2>/dev/null) || tag=
|
||||
if [[ ! -x "$version_sh" ]]; then
|
||||
echo "Error: version.sh not found next to publish_docker.sh: $version_sh" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "$tag" ]]; then
|
||||
if [[ -z "$project_root" ]]; then
|
||||
echo "Error: IMAGE_TAG (or --tag) is required outside a git repository." >&2
|
||||
exit 2
|
||||
fi
|
||||
tag=$("$version_sh" -C "$project_root" --docker) || {
|
||||
echo "Error: failed to derive IMAGE_TAG from Git ancestry." >&2
|
||||
exit 2
|
||||
}
|
||||
fi
|
||||
if [[ "$tag" == *:* || "$tag" == */* ]]; then
|
||||
echo "Error: tag must not contain : or /: $tag" >&2
|
||||
exit 2
|
||||
fi
|
||||
# Official-shaped tags (X.Y.Z or vX.Y.Z) are only legal on that exact Git tag.
|
||||
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ && -x "$version_sh" && -n "$project_root" ]]; then
|
||||
derived=$("$version_sh" -C "$project_root" --docker) || true
|
||||
expected=${tag#v}
|
||||
if [[ "$derived" != "$expected" ]]; then
|
||||
echo "Error: IMAGE_TAG $tag looks official but HEAD is $derived" >&2
|
||||
echo "Official X.Y.Z is allowed only when HEAD exact-matches vX.Y.Z." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
if [[ "$tag" == latest && ${ALLOW_LATEST:-0} != 1 && "$mode" == push ]]; then
|
||||
echo "Error: refusing to publish floating tag 'latest'; pass an explicit version." >&2
|
||||
echo "Set ALLOW_LATEST=1 only when the user explicitly asked for 'latest'." >&2
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# Include from a project Makefile. Sets VERSION (canonical, no leading v)
|
||||
# and IMAGE_TAG (Docker rendering) via builder version.sh.
|
||||
#
|
||||
# Command-line / environment VERSION= is passed as --version (official X.Y.Z
|
||||
# only when HEAD exact-matches that tag). Unset or empty VERSION is derived.
|
||||
#
|
||||
# include $(HOME)/.skills/skills/builder/scripts/version.mk
|
||||
|
||||
_builder_scripts_dir := $(dir $(lastword $(MAKEFILE_LIST)))
|
||||
|
||||
ifeq ($(BUILDER_VERSION_SH),)
|
||||
BUILDER_VERSION_SH := $(wildcard $(_builder_scripts_dir)version.sh)
|
||||
endif
|
||||
ifeq ($(BUILDER_VERSION_SH),)
|
||||
ifneq ($(BUILDER_SKILL_DIR),)
|
||||
BUILDER_VERSION_SH := $(wildcard $(BUILDER_SKILL_DIR)/scripts/version.sh)
|
||||
endif
|
||||
endif
|
||||
ifeq ($(BUILDER_VERSION_SH),)
|
||||
BUILDER_VERSION_SH := $(wildcard $(HOME)/.skills/skills/builder/scripts/version.sh)
|
||||
endif
|
||||
ifeq ($(BUILDER_VERSION_SH),)
|
||||
$(error version.sh not found; set BUILDER_SKILL_DIR or clone skills to ~/.skills)
|
||||
endif
|
||||
|
||||
ifeq ($(filter command line environment,$(origin VERSION)),)
|
||||
VERSION := $(shell "$(BUILDER_VERSION_SH)")
|
||||
else ifeq ($(strip $(VERSION)),)
|
||||
VERSION := $(shell "$(BUILDER_VERSION_SH)")
|
||||
else
|
||||
VERSION := $(shell "$(BUILDER_VERSION_SH)" --version "$(VERSION)")
|
||||
endif
|
||||
ifeq ($(strip $(VERSION)),)
|
||||
$(error version.sh produced an empty version)
|
||||
endif
|
||||
|
||||
ifeq ($(origin IMAGE_TAG),undefined)
|
||||
IMAGE_TAG := $(shell "$(BUILDER_VERSION_SH)" --docker --version "$(VERSION)")
|
||||
endif
|
||||
Executable
+195
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
version.sh [-C GIT_DIR] [--docker] [--version VER]
|
||||
|
||||
Print one line: the canonical artifact version (Debian Version, no leading v).
|
||||
With --docker, print the Docker tag rendering of that version.
|
||||
|
||||
Derivation (builder contract «版本号»):
|
||||
official HEAD exact-match of vX.Y.Z → X.Y.Z
|
||||
test nearest ancestor stable tag → X.Y.Z~branch.n+gSHA
|
||||
no tag no stable tag reachable from HEAD → 0.0.0~branch+gSHA
|
||||
|
||||
Stable tags match v<major>.<minor>.<patch> only. Baseline is ancestry, not
|
||||
the highest version in the repository. This script does not fetch tags.
|
||||
|
||||
--version VER overrides derivation. Official-shaped VER (X.Y.Z, optional
|
||||
leading v) is accepted only when HEAD exact-matches that tag. Test-shaped
|
||||
VER is used as-is.
|
||||
|
||||
Branch name: git symbolic-ref, or BUILD_BRANCH / CI_COMMIT_BRANCH /
|
||||
GITHUB_REF_NAME when detached, else "detached". Sanitized to [a-z0-9-],
|
||||
max 32 characters.
|
||||
EOF
|
||||
}
|
||||
|
||||
root=.
|
||||
mode=canonical
|
||||
override=
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
-C) root=$2; shift 2 ;;
|
||||
--docker) mode=docker; shift ;;
|
||||
--version) override=$2; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
echo "Error: not a git repository: $root" >&2
|
||||
exit 2
|
||||
fi
|
||||
root=$(git -C "$root" rev-parse --show-toplevel)
|
||||
|
||||
gitc() {
|
||||
git -C "$root" "$@"
|
||||
}
|
||||
|
||||
is_stable_tag() {
|
||||
[[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
}
|
||||
|
||||
is_official_version() {
|
||||
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
}
|
||||
|
||||
# Strip one leading v if present.
|
||||
strip_v() {
|
||||
local v=$1
|
||||
if [[ "$v" == v* ]]; then
|
||||
v=${v#v}
|
||||
fi
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
sanitize_branch() {
|
||||
local b=$1
|
||||
b=$(printf '%s' "$b" | tr '[:upper:]' '[:lower:]')
|
||||
b=${b//\//-}
|
||||
b=${b//_/-}
|
||||
b=$(printf '%s' "$b" | tr -cd 'a-z0-9-')
|
||||
b=$(printf '%s' "$b" | tr -s '-')
|
||||
b=${b#-}
|
||||
b=${b%-}
|
||||
if ((${#b} > 32)); then
|
||||
b=${b:0:32}
|
||||
b=${b%-}
|
||||
fi
|
||||
if [[ -z "$b" ]]; then
|
||||
b=unknown
|
||||
fi
|
||||
printf '%s' "$b"
|
||||
}
|
||||
|
||||
branch_slug() {
|
||||
local b
|
||||
b=$(gitc rev-parse --abbrev-ref HEAD)
|
||||
if [[ "$b" == HEAD ]]; then
|
||||
b=${BUILD_BRANCH:-${CI_COMMIT_BRANCH:-${GITHUB_REF_NAME:-detached}}}
|
||||
fi
|
||||
sanitize_branch "$b"
|
||||
}
|
||||
|
||||
short_sha() {
|
||||
gitc rev-parse HEAD | cut -c1-7
|
||||
}
|
||||
|
||||
# Highest stable tag pointing at HEAD, or empty.
|
||||
official_tag_at_head() {
|
||||
local tag best=
|
||||
while IFS= read -r tag; do
|
||||
[[ -n "$tag" ]] || continue
|
||||
is_stable_tag "$tag" || continue
|
||||
if [[ -z "$best" ]]; then
|
||||
best=$tag
|
||||
elif printf '%s\n%s\n' "$best" "$tag" | sort -V | tail -n 1 | grep -qx "$tag"; then
|
||||
best=$tag
|
||||
fi
|
||||
done < <(gitc tag --points-at HEAD)
|
||||
printf '%s' "$best"
|
||||
}
|
||||
|
||||
# Stable ancestor tag with the fewest commits to HEAD (not sort -V globally).
|
||||
nearest_stable_tag() {
|
||||
local tag dist best_dist="" best_tag=""
|
||||
while IFS= read -r tag; do
|
||||
[[ -n "$tag" ]] || continue
|
||||
is_stable_tag "$tag" || continue
|
||||
dist=$(gitc rev-list --count "${tag}..HEAD")
|
||||
if [[ -z "$best_dist" ]] || ((dist < best_dist)); then
|
||||
best_dist=$dist
|
||||
best_tag=$tag
|
||||
elif ((dist == best_dist)); then
|
||||
if printf '%s\n%s\n' "$best_tag" "$tag" | sort -V | tail -n 1 | grep -qx "$tag"; then
|
||||
best_tag=$tag
|
||||
fi
|
||||
fi
|
||||
done < <(gitc tag --merged HEAD)
|
||||
printf '%s' "$best_tag"
|
||||
}
|
||||
|
||||
derive_canonical() {
|
||||
local tag base n sha branch
|
||||
tag=$(official_tag_at_head)
|
||||
if [[ -n "$tag" ]]; then
|
||||
printf '%s' "${tag#v}"
|
||||
return
|
||||
fi
|
||||
sha=$(short_sha)
|
||||
branch=$(branch_slug)
|
||||
base=$(nearest_stable_tag)
|
||||
if [[ -z "$base" ]]; then
|
||||
printf '0.0.0~%s+g%s' "$branch" "$sha"
|
||||
return
|
||||
fi
|
||||
n=$(gitc rev-list --count "${base}..HEAD")
|
||||
printf '%s~%s.%s+g%s' "${base#v}" "$branch" "$n" "$sha"
|
||||
}
|
||||
|
||||
to_docker() {
|
||||
local v=$1
|
||||
v=${v//\~/-}
|
||||
v=${v//+g/.g}
|
||||
printf '%s' "$v"
|
||||
}
|
||||
|
||||
canonical=
|
||||
if [[ -n "$override" ]]; then
|
||||
canonical=$(strip_v "$override")
|
||||
if [[ "$canonical" == *'_'* ]]; then
|
||||
echo "Error: version must not contain '_': $canonical" >&2
|
||||
exit 2
|
||||
fi
|
||||
if is_official_version "$canonical"; then
|
||||
derived=$(derive_canonical)
|
||||
if [[ "$derived" != "$canonical" ]]; then
|
||||
echo "Error: --version $canonical looks official but HEAD is $derived" >&2
|
||||
echo "Official X.Y.Z is allowed only when HEAD exact-matches vX.Y.Z." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
else
|
||||
canonical=$(derive_canonical)
|
||||
fi
|
||||
|
||||
if [[ -z "$canonical" ]]; then
|
||||
echo "Error: empty version" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$mode" == docker ]]; then
|
||||
tag=$(to_docker "$canonical")
|
||||
if [[ "$tag" == *:* || "$tag" == */* || "$tag" == *'~'* ]]; then
|
||||
echo "Error: docker tag still contains illegal characters: $tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$tag"
|
||||
else
|
||||
printf '%s\n' "$canonical"
|
||||
fi
|
||||
Reference in New Issue
Block a user