Skip to content

Commit 50268fc

Browse files
committed
fix(release): validated cross-surface states, pinned lock generator (#356 review round 7)
Both P1s + all four P2s: - P1: the release-missing branch no longer classifies an unvalidated PyPI 200 as missing-release. Surface combination moved INTO the tested reconciler: the anchor is validated and the PyPI response is schema-checked on EVERY path (urls must be a list of objects with string filenames and valid sha256 digest objects; duplicates rejected) before any classification. {"urls": []} with a missing release is 'unpublished'; a truncated/invalid body is 'invalid-response' (indeterminate, retry-first) — never destructive yank advice. New tests cover the release-present/missing x 404/empty/files/malformed x anchor-valid/invalid matrix (19 -> 34 reconciler tests). - P1: regen-locks.sh no longer executes an unpinned resolver with write access: pip-tools (+hatchling for metadata, +setuptools via --allow-unsafe) installs from the reviewed pip-tools.lock, the repository mounts READ-ONLY, outputs land in a scratch dir and only the two expected lock files are copied back. A new producers-ci lock-drift job runs the exact generator and fails on any diff. Executed end to end locally (which also surfaced a macOS Docker gotcha: /tmp and /var/folders are not shared into the VM — scratch lives under $HOME). - P2: object-shaped invalid PyPI schemas return 'invalid-response' instead of crashing (reproduced TypeError/KeyError cases now tests). - P2: the complete publish-time re-anchor path is a composed, PR-tested helper (publish_release_body.py: anchor-wheel digest -> renderer -> atomic gh release edit --notes-file --draft=false --latest=false, tested with a fake anchor wheel and stubbed gh); finalize calls it. - P2: the image self-test asserts the multi-stage property (no build/ hatchling importable, no /build or /src tree) — verified locally against the rebuilt image. - P2: reconciler docstring lists all six states. Locks regenerated via the hardened path; image rebuilt from them and the full self-test (incl. hygiene asserts) passed locally.
1 parent 0a1863b commit 50268fc

12 files changed

Lines changed: 486 additions & 57 deletions

.github/workflows/producers-ci.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,20 @@ jobs:
140140
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
141141
- name: Run the checked-in self-test (same script the release runs)
142142
run: bash deploy/otlp_receiver/selftest.sh bqaa-pr-selftest "$VERSION"
143+
144+
lock-drift:
145+
name: Hash-lock drift check
146+
runs-on: ubuntu-latest
147+
timeout-minutes: 20
148+
defaults:
149+
run:
150+
working-directory: .
151+
steps:
152+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
153+
- name: Regenerate locks with the exact pinned generator
154+
run: bash deploy/otlp_receiver/regen-locks.sh
155+
- name: Locks must match the reviewed files
156+
run: |
157+
git diff --exit-code deploy/otlp_receiver/requirements.lock \
158+
producers/build-requirements.lock \
159+
|| (echo "::error::checked-in locks drift from regen-locks.sh output — regenerate and review" && exit 1)

.github/workflows/release-tracing.yml

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -563,22 +563,21 @@ jobs:
563563
echo "::error::PyPI lookup indeterminate (HTTP ${HTTP}) — not treating as absent"
564564
exit 1
565565
fi
566+
# Surface combination lives IN the tested reconciler: the anchor
567+
# and the PyPI schema are validated on EVERY path, so a
568+
# truncated success body can never become destructive
569+
# yank/version-burn advice (#356 round 7).
566570
if [ "$RELEASE_MISSING" = "1" ]; then
567-
# Classify the cross-surface state without the release dir.
568-
if [ "$HTTP" = "404" ]; then
569-
STATE=unpublished
570-
else
571-
STATE=missing-release
572-
fi
573-
echo "GitHub release absent; PyPI HTTP ${HTTP} -> state=${STATE}"
571+
RELEASE_ARG=--release-missing
574572
else
575-
set +e
576-
python3 producers/scripts/reconcile_release.py \
577-
--version "${VERSION}" --anchor-dir anchor/ \
578-
--release-dir release/ --pypi-json pypi.json | tee reconcile.out
579-
set -e
580-
STATE=$(grep '^state=' reconcile.out | cut -d= -f2)
573+
RELEASE_ARG="--release-dir release/"
581574
fi
575+
set +e
576+
python3 producers/scripts/reconcile_release.py \
577+
--version "${VERSION}" --anchor-dir anchor/ \
578+
$RELEASE_ARG --pypi-json pypi.json | tee reconcile.out
579+
set -e
580+
STATE=$(grep '^state=' reconcile.out | cut -d= -f2)
582581
if [ "$STATE" = "complete" ]; then
583582
curl -sL -o crane.tar.gz "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz"
584583
echo "${CRANE_SHA256} crane.tar.gz" | sha256sum -c -
@@ -613,25 +612,16 @@ jobs:
613612
EXIT_CODE=$(echo "$VERDICT" | cut -d'|' -f2)
614613
MESSAGE=$(echo "$VERDICT" | cut -d'|' -f3-)
615614
if [ "$PUBLISH" = "1" ]; then
616-
# Re-anchor the customer-facing body: asset reconciliation
617-
# never reads the draft body, and anything with contents:write
618-
# could have edited it during the approval pause. Re-render
619-
# from the protected checkout with the digest extracted from
620-
# the ANCHOR wheel, then publish atomically.
621-
ANCHOR_DIGEST=$(python3 -c "
622-
import pathlib, sys
623-
sys.path.insert(0, 'producers/scripts')
624-
import release_image_tool
625-
ref = release_image_tool.extract_from_wheel(pathlib.Path(
626-
'anchor/bigquery_agent_analytics_tracing-${{ needs.verify.outputs.version }}-py3-none-any.whl'))
627-
print(ref.split('@')[1])")
628-
python3 producers/scripts/render_release_notes.py \
615+
# Re-anchor the customer-facing body via the PR-TESTED
616+
# composed helper (anchor-wheel digest -> renderer -> atomic
617+
# gh release edit): anything with contents:write could have
618+
# edited the draft body during the approval pause.
619+
python3 producers/scripts/publish_release_body.py \
629620
--version "${{ needs.verify.outputs.version }}" \
630-
--digest "$ANCHOR_DIGEST" \
631621
--public-image "${{ env.PUBLIC_IMAGE }}" \
632-
--out release_body.md
633-
gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" \
634-
--notes-file release_body.md --draft=false --latest=false
622+
--anchor-dir anchor/ \
623+
--tag "${GITHUB_REF_NAME}" \
624+
--repo "${GITHUB_REPOSITORY}"
635625
echo "release published with re-anchored body: ${MESSAGE}"
636626
else
637627
echo "::error::${MESSAGE}"

deploy/otlp_receiver/pip-tools.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Generator env for regen-locks.sh: resolver + the build backend needed
2+
# to read producers/pyproject.toml metadata without isolation.
3+
pip-tools
4+
hatchling
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#
2+
# This file is autogenerated by pip-compile with Python 3.12
3+
# by the following command:
4+
#
5+
# pip-compile --allow-unsafe --generate-hashes --output-file=deploy/otlp_receiver/pip-tools.lock --strip-extras deploy/otlp_receiver/pip-tools.in
6+
#
7+
build==1.5.1 \
8+
--hash=sha256:94e17f1db803ab22f46049376c44c8437c52090f0dfdf1adc43df56542d644fb \
9+
--hash=sha256:f1a58fe2e5af5b0238a07b9e70207492c79ddebbdb1ad954fc86d62a56be3e0d
10+
# via pip-tools
11+
click==8.4.2 \
12+
--hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \
13+
--hash=sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76
14+
# via pip-tools
15+
hatchling==1.31.0 \
16+
--hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \
17+
--hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544
18+
# via -r deploy/otlp_receiver/pip-tools.in
19+
packaging==26.2 \
20+
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
21+
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
22+
# via
23+
# build
24+
# hatchling
25+
# wheel
26+
pathspec==1.1.1 \
27+
--hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
28+
--hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
29+
# via hatchling
30+
pip-tools==7.5.3 \
31+
--hash=sha256:3aac0c473240ae90db7213c033401f345b05197293ccbdd2704e52e7a783785e \
32+
--hash=sha256:8fa364779ebc010cbfe17cb9de404457ac733e100840423f28f6955de7742d41
33+
# via -r deploy/otlp_receiver/pip-tools.in
34+
pluggy==1.6.0 \
35+
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
36+
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
37+
# via hatchling
38+
pyproject-hooks==1.2.0 \
39+
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
40+
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
41+
# via
42+
# build
43+
# pip-tools
44+
trove-classifiers==2026.6.1.19 \
45+
--hash=sha256:ab4c4ec93cc4a4e7815fa759906e05e6bb3f2fbd92ea0f897288c6a43efd15b3 \
46+
--hash=sha256:c5132b4b61a829d11cfbd2d72e97f20a45ed6edb95e45c5efdeb5e00836b2745
47+
# via hatchling
48+
wheel==0.47.0 \
49+
--hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced \
50+
--hash=sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3
51+
# via pip-tools
52+
53+
# The following packages are considered to be unsafe in a requirements file:
54+
pip==26.1.2 \
55+
--hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \
56+
--hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605
57+
# via pip-tools
58+
setuptools==83.0.0 \
59+
--hash=sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef \
60+
--hash=sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3
61+
# via pip-tools
Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
#!/usr/bin/env bash
2-
# Regenerate BOTH hash-locked dependency files inside the exact pinned
3-
# base image the Dockerfile uses, so the resolution platform matches
4-
# production. Run from the repository root; review the diffs.
2+
# Regenerate the hash-locked dependency files inside the exact pinned
3+
# base image the Dockerfile uses. Supply-chain posture (#356 round 7):
4+
# the generator itself (pip-tools) installs from a reviewed hash lock
5+
# (pip-tools.lock — the one acknowledged bootstrap input); the
6+
# repository mounts READ-ONLY; outputs are produced in a scratch
7+
# directory and only the expected lock files are copied back after the
8+
# container exits.
59
set -euo pipefail
610
BASE="python:3.12-slim@sha256:423ed6ab25b1921a477529254bfeeabf5855151dc2c3141699a1bfc852199fbf"
7-
docker run --rm -v "$PWD:/src" "$BASE" bash -c "
8-
pip install -q pip-tools &&
9-
cd /src &&
11+
# Scratch lives under $HOME: macOS Docker Desktop shares /Users but not
12+
# /tmp or /var/folders — a mount outside the share silently swallows the
13+
# outputs inside the VM.
14+
mkdir -p "$HOME/.cache"
15+
OUT=$(mktemp -d "$HOME/.cache/bqaa-regen-locks.XXXXXX")
16+
docker run --rm -v "$PWD:/src:ro" -v "$OUT:/out" "$BASE" bash -c "
17+
set -euo pipefail
18+
pip install -q --require-hashes -r /src/deploy/otlp_receiver/pip-tools.lock &&
1019
pip-compile --quiet --generate-hashes --strip-extras --extra receiver \
11-
--output-file deploy/otlp_receiver/requirements.lock \
12-
producers/pyproject.toml &&
20+
--no-build-isolation \
21+
--output-file /out/requirements.lock \
22+
/src/producers/pyproject.toml &&
1323
pip-compile --quiet --generate-hashes --strip-extras \
14-
--output-file producers/build-requirements.lock \
15-
deploy/otlp_receiver/build-toolchain.in
24+
--output-file /out/build-requirements.lock \
25+
/src/deploy/otlp_receiver/build-toolchain.in
1626
"
27+
cp "$OUT/requirements.lock" deploy/otlp_receiver/requirements.lock
28+
cp "$OUT/build-requirements.lock" producers/build-requirements.lock
29+
rm -rf "$OUT"
1730
echo "locks regenerated — review the diffs before committing"

deploy/otlp_receiver/requirements.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --extra=receiver --generate-hashes --output-file=deploy/otlp_receiver/requirements.lock --strip-extras producers/pyproject.toml
5+
# pip-compile --extra=receiver --generate-hashes --no-build-isolation --output-file=/out/requirements.lock --strip-extras /src/producers/pyproject.toml
66
#
77
certifi==2026.6.17 \
88
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
@@ -271,15 +271,15 @@ google-auth==2.55.2 \
271271
google-cloud-bigquery==3.42.2 \
272272
--hash=sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39 \
273273
--hash=sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f
274-
# via bigquery-agent-analytics-tracing (producers/pyproject.toml)
274+
# via bigquery-agent-analytics-tracing (/src/producers/pyproject.toml)
275275
google-cloud-core==2.6.0 \
276276
--hash=sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e \
277277
--hash=sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83
278278
# via google-cloud-bigquery
279279
google-cloud-pubsub==2.39.0 \
280280
--hash=sha256:7210d691a46d7a66559696899ebe6eb731e63de29b624964b3be4dd2d12d3e19 \
281281
--hash=sha256:eed65e25f57f95bf3e02d96d7ee171688b23922471f9f21b5a91ed90e1282c0f
282-
# via bigquery-agent-analytics-tracing (producers/pyproject.toml)
282+
# via bigquery-agent-analytics-tracing (/src/producers/pyproject.toml)
283283
google-crc32c==1.8.0 \
284284
--hash=sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8 \
285285
--hash=sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a \
@@ -397,7 +397,7 @@ grpcio-status==1.82.1 \
397397
gunicorn==26.0.0 \
398398
--hash=sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc \
399399
--hash=sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf
400-
# via bigquery-agent-analytics-tracing (producers/pyproject.toml)
400+
# via bigquery-agent-analytics-tracing (/src/producers/pyproject.toml)
401401
idna==3.18 \
402402
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
403403
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
@@ -412,7 +412,7 @@ opentelemetry-api==1.43.0 \
412412
opentelemetry-proto==1.43.0 \
413413
--hash=sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924 \
414414
--hash=sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d
415-
# via bigquery-agent-analytics-tracing (producers/pyproject.toml)
415+
# via bigquery-agent-analytics-tracing (/src/producers/pyproject.toml)
416416
opentelemetry-sdk==1.43.0 \
417417
--hash=sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823 \
418418
--hash=sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9

deploy/otlp_receiver/selftest.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ assert v == '${VERSION}', f'image has {v}, want ${VERSION}'
3535
from bigquery_agent_analytics_tracing.otlp import app, consumer
3636
assert callable(app.make_app)
3737
assert callable(consumer.make_push_app_from_env)
38-
print('version + factories ok:', v)
38+
# Multi-stage property: the runtime image must carry neither the build
39+
# toolchain nor the source tree — only the installed package.
40+
import importlib.util, pathlib
41+
assert importlib.util.find_spec('build') is None, 'build tool in runtime image'
42+
assert importlib.util.find_spec('hatchling') is None, 'hatchling in runtime image'
43+
assert not pathlib.Path('/build').exists(), 'builder stage leaked /build'
44+
assert not pathlib.Path('/src').exists(), 'source tree leaked /src'
45+
print('version + factories + multi-stage hygiene ok:', v)
3946
"
4047

4148
echo "==> private bridge network + pinned emulator"

producers/build-requirements.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
# This file is autogenerated by pip-compile with Python 3.12
33
# by the following command:
44
#
5-
# pip-compile --generate-hashes --output-file=producers/build-requirements.lock --strip-extras build-toolchain.in
5+
# pip-compile --generate-hashes --output-file=/out/build-requirements.lock --strip-extras /src/deploy/otlp_receiver/build-toolchain.in
66
#
77
build==1.5.1 \
88
--hash=sha256:94e17f1db803ab22f46049376c44c8437c52090f0dfdf1adc43df56542d644fb \
99
--hash=sha256:f1a58fe2e5af5b0238a07b9e70207492c79ddebbdb1ad954fc86d62a56be3e0d
10-
# via -r build-toolchain.in
10+
# via -r /src/deploy/otlp_receiver/build-toolchain.in
1111
hatchling==1.31.0 \
1212
--hash=sha256:6b48ad4068a482ed7239b3a8215bc55b47aad3345d58dfc94e553c5d2d46211b \
1313
--hash=sha256:aac80bec8b6fe35e8480f1c335be8910fa210a0e6f735a139be205dadcacb544
14-
# via -r build-toolchain.in
14+
# via -r /src/deploy/otlp_receiver/build-toolchain.in
1515
packaging==26.2 \
1616
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
1717
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Publish-time body re-anchor (#356 rounds 6–7), as one tested path.
16+
17+
Asset reconciliation never reads the draft body, and anything holding
18+
``contents: write`` could have edited it during the approval pause. This
19+
helper composes the full re-anchor: extract the digest from the ANCHOR
20+
wheel → render the notes from the protected checkout → publish
21+
atomically with ``gh release edit --notes-file … --draft=false
22+
--latest=false``. PR-tested with a fake anchor wheel and a stubbed gh.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import argparse
28+
import pathlib
29+
import subprocess
30+
import sys
31+
from typing import Callable
32+
33+
sys.path.insert(0, str(pathlib.Path(__file__).parent))
34+
import release_image_tool
35+
import render_release_notes
36+
37+
38+
def _run_gh(argv: list[str]) -> int:
39+
return subprocess.run(argv, check=False).returncode
40+
41+
42+
def publish(
43+
*,
44+
version: str,
45+
public_image: str,
46+
anchor_dir: pathlib.Path,
47+
tag: str,
48+
repo: str,
49+
out_dir: pathlib.Path,
50+
run_gh: Callable[[list[str]], int] = _run_gh,
51+
) -> int:
52+
wheel = (
53+
anchor_dir
54+
/ f"bigquery_agent_analytics_tracing-{version}-py3-none-any.whl"
55+
)
56+
if not wheel.is_file():
57+
raise FileNotFoundError(f"anchor wheel not found: {wheel}")
58+
reference = release_image_tool.extract_from_wheel(wheel)
59+
digest = reference.split("@", 1)[1]
60+
body_path = out_dir / "release_body.md"
61+
body_path.write_text(
62+
render_release_notes.render(
63+
version=version, digest=digest, public_image=public_image
64+
)
65+
)
66+
return run_gh(
67+
[
68+
"gh",
69+
"release",
70+
"edit",
71+
tag,
72+
"--repo",
73+
repo,
74+
"--notes-file",
75+
str(body_path),
76+
"--draft=false",
77+
"--latest=false",
78+
]
79+
)
80+
81+
82+
def main(argv: list[str] | None = None) -> int:
83+
parser = argparse.ArgumentParser(description=__doc__)
84+
parser.add_argument("--version", required=True)
85+
parser.add_argument("--public-image", required=True)
86+
parser.add_argument("--anchor-dir", type=pathlib.Path, required=True)
87+
parser.add_argument("--tag", required=True)
88+
parser.add_argument("--repo", required=True)
89+
parser.add_argument("--out-dir", type=pathlib.Path, default=pathlib.Path("."))
90+
args = parser.parse_args(argv)
91+
return publish(
92+
version=args.version,
93+
public_image=args.public_image,
94+
anchor_dir=args.anchor_dir,
95+
tag=args.tag,
96+
repo=args.repo,
97+
out_dir=args.out_dir,
98+
)
99+
100+
101+
if __name__ == "__main__":
102+
raise SystemExit(main())

0 commit comments

Comments
 (0)