Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
df2db57
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration …
github-actions[bot] Jun 11, 2026
bf5ad77
ci: trigger checks
github-actions[bot] Jun 11, 2026
701b6aa
Merge origin/main and resolve marketplace conflict
Copilot Jun 15, 2026
68ce895
ci: trigger checks
github-actions[bot] Jun 15, 2026
1b6a367
ci: trigger checks
github-actions[bot] Jun 15, 2026
85eb2ad
fix(migration-ci): fix upstream freshness ancestor check and stale sc…
github-actions[bot] Jun 15, 2026
1e52f3b
ci: trigger checks
github-actions[bot] Jun 15, 2026
3178192
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] fix(go-mig…
github-actions[bot] Jun 15, 2026
6fec725
ci: trigger checks
github-actions[bot] Jun 15, 2026
089ebaa
Merge origin/main and resolve scheduler test conflict
Copilot Jun 15, 2026
5aea147
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] fix(go-mig…
github-actions[bot] Jun 18, 2026
f6e612a
ci: trigger checks
github-actions[bot] Jun 18, 2026
25214e9
fix(go-migration): add benchmark context Go test coverage and advance…
github-actions[bot] Jun 18, 2026
20f98f5
ci: trigger checks
github-actions[bot] Jun 18, 2026
ae46922
fix(go-migration): apply migration-ci.yml benchmark context from main…
github-actions[bot] Jun 18, 2026
e8d5ca3
ci: trigger checks
github-actions[bot] Jun 18, 2026
4b02537
fix: use colon format for unknown-option errors in experimental commands
github-actions[bot] Jun 19, 2026
1d013a4
ci: trigger checks
github-actions[bot] Jun 19, 2026
7904273
fix: reject unknown options in all Go commands to match Python Click …
github-actions[bot] Jun 20, 2026
4f5943b
ci: trigger checks
github-actions[bot] Jun 20, 2026
6914e84
[Crane: crane-migration-python-to-go-full-apm-cli-rewrite] Iteration …
github-actions[bot] Jun 20, 2026
ce1121c
ci: trigger checks
github-actions[bot] Jun 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions .github/workflows/migration-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,19 +324,207 @@ jobs:
cat "$RUNNER_TEMP/migration-cli-benchmark.md" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Download parity evidence
if: always()
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: migration-parity-evidence
path: ${{ runner.temp }}/migration-parity-evidence

- name: Post benchmark PR comment
if: always() && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PARITY_RESULT: ${{ needs.parity.result }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
if [ ! -f "$RUNNER_TEMP/migration-cli-benchmark.md" ]; then
echo "No migration benchmark markdown found; skipping PR comment."
exit 0
fi

SCORE_PATH="$RUNNER_TEMP/migration-parity-evidence/migration-score.json"
export SCORE_PATH

python - <<'PY' > "$RUNNER_TEMP/migration-benchmark-context.md"
from __future__ import annotations

import json
import os
import subprocess
from pathlib import Path


def gh_json(path: str) -> object | None:
try:
completed = subprocess.run(
["gh", "api", path],
check=True,
capture_output=True,
encoding="utf-8",
)
except subprocess.CalledProcessError:
return None
return json.loads(completed.stdout)


def subject(message: str) -> str:
return message.splitlines()[0] if message else "(no commit subject)"


def body_lines(message: str, limit: int = 5) -> list[str]:
items: list[str] = []
current: list[str] = []
for raw_line in message.splitlines()[1:]:
line = raw_line.strip()
if not line or line.startswith("Co-authored-by:"):
continue
if line.startswith(("Fixes ", "Run:")):
continue
is_bullet = line.startswith(("-", "*"))
cleaned = line.lstrip("-* ").strip()
if is_bullet:
if current:
items.append(" ".join(current))
current = [cleaned]
elif current:
current.append(cleaned)
else:
current = [cleaned]
if current:
items.append(" ".join(current))
return items[:limit]


def short(sha: str | None) -> str:
return (sha or "")[:7]


def is_trigger_only(message: str) -> bool:
return subject(message).strip().lower() in {"ci: trigger checks"}


def bool_word(value: object) -> str:
return "yes" if value is True else "no" if value is False else "unknown"


def format_float(value: object) -> str:
if isinstance(value, int | float):
return f"{value:.3f}".rstrip("0").rstrip(".")
return "unknown"


repo = os.environ["GITHUB_REPOSITORY"]
pr_number = os.environ["PR_NUMBER"]
head_sha = os.environ["HEAD_SHA"]
parity_result = os.environ.get("PARITY_RESULT", "unknown")
score_path = Path(os.environ["SCORE_PATH"])

commits = gh_json(f"repos/{repo}/pulls/{pr_number}/commits?per_page=100")
commits = commits if isinstance(commits, list) else []
head_commit = next((item for item in commits if item.get("sha") == head_sha), None)
if head_commit is None and commits:
head_commit = commits[-1]
head_message = ((head_commit or {}).get("commit") or {}).get("message", "")

change_commit = head_commit
if is_trigger_only(head_message):
for item in reversed(commits[:-1]):
message = (item.get("commit") or {}).get("message", "")
if not is_trigger_only(message):
change_commit = item
break

change_sha = (change_commit or {}).get("sha") or head_sha
change_message = ((change_commit or {}).get("commit") or {}).get("message", "")
commit_detail = gh_json(f"repos/{repo}/commits/{change_sha}") or {}
files = [item.get("filename", "") for item in commit_detail.get("files", [])]
files = [filename for filename in files if filename]

print("### What changed")
print()
print(f"- **PR head**: `{short(head_sha)}` -- {subject(head_message)}")
if change_sha != head_sha:
print(
f"- **Change commit**: `{short(change_sha)}` -- "
f"{subject(change_message)} (latest non-trigger commit)"
)
notes = body_lines(change_message)
if notes:
print("- **Commit notes**:")
for line in notes:
print(f" - {line}")
if files:
shown = files[:10]
extra = len(files) - len(shown)
suffix = f", +{extra} more" if extra > 0 else ""
print(f"- **Files touched**: {', '.join(f'`{name}`' for name in shown)}{suffix}")
print()

score: dict[str, object] = {}
if score_path.is_file():
score = json.loads(score_path.read_text(encoding="utf-8"))

print("### Parity snapshot")
print()
if score:
gates = score.get("gates") or []
failing = [
str(gate.get("name"))
for gate in gates
if isinstance(gate, dict) and gate.get("passing") is False
]
parity_passing = score.get("parity_passing", "?")
parity_total = score.get("parity_total", "?")
print(f"- **Score**: {format_float(score.get('migration_score'))}")
print(f"- **Progress**: {format_float(score.get('progress'))}")
print(f"- **Parity**: {parity_passing}/{parity_total}")
print(
"- **Tests**: "
f"Go {score.get('target_tests_passing', '?')}, "
f"Python {score.get('source_tests_passing', '?')}"
)
print(f"- **Deletion-grade ready**: {bool_word(score.get('deletion_grade_ready'))}")
print(f"- **Blocking gates**: {', '.join(failing) if failing else 'none'}")
else:
failing = []
print(f"- **Parity job result**: {parity_result}")
print("- **Score artifact**: unavailable")
print()

print("### Next work")
print()
failing_set = set(failing)
if score and not failing_set and score.get("deletion_grade_ready") is True:
print("- No benchmark or parity follow-up is needed; proceed to the completion gate.")
elif failing_set & {"upstream_freshness", "upstream_contracts"}:
print(
"- Refresh the upstream APM baseline/reviewed SHA and repair upstream "
"contract coverage until `upstream_freshness` and `upstream_contracts` pass."
)
elif failing_set & {
"surface_parity",
"help_parity",
"option_parity",
"functional_contracts",
"state_diff_contracts",
"python_behavior_contracts",
}:
print(
"- Fix the listed Python/Go contract drift, add or update parity coverage, "
"and rerun migration CI."
)
elif failing_set & {"benchmarks_pass"}:
print("- Investigate the benchmark regression and restore Go/Python return-code parity.")
elif score:
print("- Inspect the failing gate artifacts and turn the first failing gate into the next Crane task.")
else:
print("- Open the parity evidence artifact; the score summary was not available to this job.")
PY

marker="<!-- apm-migration-benchmark:${HEAD_SHA} -->"
{
echo "$marker"
Expand All @@ -345,6 +533,8 @@ jobs:
echo "- **Commit**: \`${HEAD_SHA}\`"
echo "- **Run**: ${RUN_URL}"
echo
cat "$RUNNER_TEMP/migration-benchmark-context.md"
echo
cat "$RUNNER_TEMP/migration-cli-benchmark.md"
} > "$RUNNER_TEMP/migration-benchmark-pr-comment.md"

Expand Down
9 changes: 8 additions & 1 deletion cmd/apm/cmd_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ func runAudit(args []string) int {
i++
}
default:
if !startsWith(args[i], "-") && pkg == "" {
if startsWith(args[i], "--target=") || startsWith(args[i], "--runtime=") ||
startsWith(args[i], "--exclude=") || startsWith(args[i], "--only=") {
// known key=value flags
} else if startsWith(args[i], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[i])
fmt.Fprintln(os.Stderr, `Try 'apm audit --help' for help.`)
return 2
} else if pkg == "" {
pkg = args[i]
}
}
Expand Down
38 changes: 37 additions & 1 deletion cmd/apm/cmd_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ func runCache(args []string) int {
return 0
}

if startsWith(args[0], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[0])
fmt.Fprintln(os.Stderr, `Try 'apm cache --help' for help.`)
return 2
}

sub := args[0]
rest := args[1:]

Expand Down Expand Up @@ -80,6 +86,11 @@ func runCacheInfo(args []string) int {
fmt.Println(" --help Show this message and exit.")
return 0
}
if startsWith(a, "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", a)
fmt.Fprintln(os.Stderr, `Try 'apm cache info --help' for help.`)
return 2
}
}
dir := cacheDir()
size := dirSize(dir)
Expand Down Expand Up @@ -115,6 +126,16 @@ func runCacheClean(args []string) int {
fmt.Println(" --help Show this message and exit.")
return 0
}
switch a {
case "-f", "--force", "-y", "--yes":
// known flags
default:
if startsWith(a, "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", a)
fmt.Fprintln(os.Stderr, `Try 'apm cache clean --help' for help.`)
return 2
}
}
}
dir := cacheDir()
entries, err := os.ReadDir(dir)
Expand Down Expand Up @@ -143,7 +164,8 @@ func runCacheClean(args []string) int {
}

func runCachePrune(args []string) int {
for _, a := range args {
for i := 0; i < len(args); i++ {
a := args[i]
if a == "--help" || a == "-h" {
fmt.Println("Usage: apm cache prune [OPTIONS]")
fmt.Println()
Expand All @@ -155,6 +177,20 @@ func runCachePrune(args []string) int {
fmt.Println(" --help Show this message and exit.")
return 0
}
if a == "--days" {
if i+1 < len(args) {
i++
}
continue
}
if startsWith(a, "--days=") {
continue
}
if startsWith(a, "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", a)
fmt.Fprintln(os.Stderr, `Try 'apm cache prune --help' for help.`)
return 2
}
}
fmt.Println("[*] Pruning old cache entries...")
fmt.Println("[+] Cache pruned.")
Expand Down
4 changes: 4 additions & 0 deletions cmd/apm/cmd_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func runCompile(args []string) int {
default:
if startsWith(args[i], "--target=") {
target = args[i][9:]
} else if startsWith(args[i], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[i])
fmt.Fprintln(os.Stderr, `Try 'apm compile --help' for help.`)
return 2
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions cmd/apm/cmd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ func runConfig(args []string) int {
return 0
}

if startsWith(args[0], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[0])
fmt.Fprintln(os.Stderr, `Try 'apm config --help' for help.`)
return 2
}

switch args[0] {
case "set":
return runConfigSet(args[1:])
Expand All @@ -67,6 +73,13 @@ func runConfigSet(args []string) int {
fmt.Println(" --help Show this message and exit.")
return 0
}
for _, a := range args {
if startsWith(a, "-") && a != "--help" && a != "-h" {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", a)
fmt.Fprintln(os.Stderr, `Try 'apm config set --help' for help.`)
return 2
}
}
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Error: Missing KEY and VALUE arguments.")
fmt.Fprintln(os.Stderr, `Usage: apm config set KEY VALUE`)
Expand Down Expand Up @@ -105,6 +118,11 @@ func runConfigGet(args []string) int {
fmt.Fprintln(os.Stderr, "Error: Missing KEY argument.")
return 2
}
if startsWith(args[0], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[0])
fmt.Fprintln(os.Stderr, `Try 'apm config get --help' for help.`)
return 2
}
key := args[0]
if !validConfigKeys[key] {
fmt.Fprintf(os.Stderr, "[x] Unknown configuration key: '%s'\n", key)
Expand Down Expand Up @@ -140,6 +158,11 @@ func runConfigUnset(args []string) int {
fmt.Fprintln(os.Stderr, "Error: Missing KEY argument.")
return 2
}
if startsWith(args[0], "-") {
fmt.Fprintf(os.Stderr, "Error: No such option: %s\n", args[0])
fmt.Fprintln(os.Stderr, `Try 'apm config unset --help' for help.`)
return 2
}
key := args[0]
if !validConfigKeys[key] {
fmt.Fprintf(os.Stderr, "[x] Unknown configuration key: '%s'\n", key)
Expand Down
Loading
Loading