GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — devstack/scripts/lpb-devstack at dev · lpb-stack/devstack

Expand file tree

/

lpb-devstack

Copy path
More file actions
More file actions

Latest commit

 

History

History
History
executable file
·
461 lines (400 loc) · 20.2 KB
/

lpb-devstack

Copy path

File metadata and controls

executable file
·
461 lines (400 loc) · 20.2 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
#!/usr/bin/env python3
"""lpb-devstack — LocalPibox DevOps workspace tool.
For developers working on the stack (workspace + GitHub). Config repo
management inside the container lives in lpb-config.
Usage:
lpb-devstack [--tag dev|main] [--devstack-dir PATH] <command>
--devstack-dir PATH devstack checkout to work on. The checkout is found by
content, so its directory name is free (a worktree
named anything works); when the workspace holds more
than one, the command asks — or pin it here.
lpb-devstack bump [--minor|--major] [--set V] [--no-commit] [--push]
Bump devstack/VERSION (default: patch, suffix preserved) and commit.
lpb-devstack tag-repos [--branch dev|main] [--version V] [--dry-run]
Tag the 5 stack repos (excl. devstack) at the given version.
lpb-devstack workspace status
Show workspace repo branches + alignment.
lpb-devstack workspace sync
Clone/symlink/align/pull workspace repos (the single write path). The
devstack repo is located by content, not by directory name.
lpb-devstack validate
Full stack alignment check (repos, branches, pins, env).
lpb-devstack release status
Stable-release readiness across all 6 repos + docs flag (non-destructive).
lpb-devstack release docs-ready [--yes]
Flag the docs branch as reviewed for the next stable release
(merge dev→docs, build site, confirm, commit DOCS_READY + push).
lpb-devstack release promote [--yes] [--dry-run] [--rebase] [--force]
Promote dev branches to stable branches + push (blocked until docs
are flagged ready; --force overrides).
lpb-devstack validate-hooks
Run the full devstack pre-commit checks (tests included).
Pipeline detection:
Read from the VERSION file (-lpb-dev → dev, else main). Override with --tag.
Environment:
LPB_WORKSPACE_ROOT — Workspace root (default: /home/lpb/workspace)
LPB_DEVSTACK_DIR — devstack checkout (same as --devstack-dir)
AGENT_DIR — Config repo path (default: /home/lpb/.pi/agent)
LPB_AGENT_GIT — Extension clone dir (default: $AGENT_DIR/git/github.com/lpb-stack)
LPB_STACK_REMOTE_BASE — Stack remote base (default: https://github.com/lpb-stack)
LPB_GITHUB_TOKEN — GitHub token for authenticated git operations
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
_SELF_DIR = Path(__file__).resolve().parent
# Shared localpibox package location — repo layout (support/ → <devstack>/scripts/),
# Docker image (/opt/pi-support), or host install (~/.lpb-stack/devstack/).
for _c in (
_SELF_DIR.parent / "scripts",
_SELF_DIR,
Path("/opt/pi-support"),
Path.home() / ".lpb-stack" / "devstack",
):
if (_c / "localpibox").is_dir():
sys.path.insert(0, str(_c))
break
from localpibox.cli import install_sigpipe_handler # noqa: E402
from localpibox.log import Console # noqa: E402
from localpibox.run import run_cmd # noqa: E402
from localpibox.stack.gitutil import git, git_auth # noqa: E402
from localpibox.stack.repos import TAG_REPOS, set_devstack_dir # noqa: E402
from localpibox.stack.version import ( # noqa: E402
VERSION_RE,
_devstack_root_candidates,
_find_version_file,
bump_version,
detect_pipeline,
get_version,
)
from localpibox.stack.workspace import ( # noqa: E402
_resolve_repo_path,
cmd_workspace_status,
cmd_workspace_sync,
sync_pins_quiet,
)
from localpibox.stack.validate import cmd_validate # noqa: E402
from localpibox.stack.release import ( # noqa: E402
cmd_release_docs_ready,
cmd_release_promote,
cmd_release_status,
dev_release_gate,
)
# ─── VERSION bumping ───────────────────────────────────────────────────────
def cmd_bump(cons: Console, *, minor: bool = False, major: bool = False,
set_version: str | None = None,
no_commit: bool = False, push: bool = False,
force: bool = False) -> int:
"""Bump devstack/VERSION and commit it (push is manual — CI builds on it)."""
vf = _find_version_file()
if vf is None:
cons.error("VERSION file not found (looked in the devstack root, "
"/opt/devstack, and $LPB_WORKSPACE_ROOT/devstack)")
return 1
current = vf.read_text().strip()
if set_version is not None:
if not VERSION_RE.match(set_version):
cons.error(f"invalid version: {set_version!r} (expected 0.x.y-lpb[-dev])")
return 1
new = set_version
else:
kind = "major" if major else ("minor" if minor else "patch")
try:
new = bump_version(current, kind)
except ValueError as e:
cons.error(str(e))
cons.info(f"Use --set 0.x.y-lpb[-dev] to set an explicit version.")
return 1
if new == current:
cons.warn(f"VERSION unchanged ({current})")
return 0
# Dev release gate: CI tags the REMOTE dev heads of the other repos, so
# unpushed/uncommitted local work would ship invisibly. Block unless --force.
if not force:
problems = dev_release_gate(cons)
if problems:
cons.error(f"Dev release gate: {problems} problem(s) — fix and re-run, "
f"or bypass with --force")
return 1
else:
cons.warn("Dev release gate skipped (--force)")
vf.write_text(new + "\n")
cons.info(f"VERSION: {current} → {new}")
# Keep settings.json pins in lockstep with the new VERSION, BEFORE the
# commit: the pre-commit validate checks pins == VERSION, and syncing
# after the hook would target the OLD version (the hook runs before the
# bump lands). Without this, the first dev bump after a stable release
# (pins still at the stable tag) fails its own gate.
from localpibox.stack.repos import DEFAULT_AGENT_DIR as _agent_dir
n = sync_pins_quiet(_agent_dir, new, cons)
if n:
cons.info(f"Synced {n} extension pin(s) to {new} "
f"(run 'pi update --extensions' after the tags exist)")
if no_commit:
cons.info("--no-commit: file written, not committed (review + commit manually)")
return 0
repo_root = vf.parent
out, _, code = git(repo_root, "rev-parse", "--is-inside-work-tree")
if code != 0 or out.strip() != "true":
cons.error(f"{repo_root} is not a git worktree — commit the VERSION change manually")
return 1
git(repo_root, "add", "VERSION")
cons.info("Committing (pre-commit hook runs the test suite — may take a minute)...")
out, err, code = git(repo_root, "commit", "-m", f"chore: bump VERSION {new}",
timeout=900)
if code != 0:
cons.error(f"Commit failed: {(err.strip() or out.strip())[:200]}")
return 1
cons.info(f"Committed: chore: bump VERSION {new}")
if push:
branch_out, _, bcode = git(repo_root, "branch", "--show-current")
branch = branch_out.strip()
if bcode != 0 or not branch:
cons.error("Not on a branch — push manually (git push origin <branch>)")
return 1
out, err, code = git_auth(repo_root, "push", "origin", branch, timeout=180)
if code != 0:
cons.error(f"Push failed: {(err.strip() or out.strip())[:200]}")
return 1
cons.info(f"Pushed {branch} — CI will build + tag {new}")
else:
cons.info("Push to trigger the CI build + tag: git push origin <branch>")
cons.warn("The bump must be the TIP of the push: CI's VERSION_CHECK diffs "
"the pushed tip commit for VERSION changes. Commits made after "
"this one make the release a no-op (tests-only run) — if you "
"add follow-up commits, run 'lpb-devstack bump' again so the "
"bump is pushed last.")
return 0
# ─── Repo tagging ──────────────────────────────────────────────────────────
def _tag_repo_hint(name: str) -> str:
return ("$AGENT_DIR" if name == "config"
else f"$LPB_WORKSPACE_ROOT/{name}")
def cmd_tag_repos(cons: Console, *, pipeline: str = "dev",
version: str | None = None, dry_run: bool = False) -> int:
"""Tag the 5 stack repos (excl. devstack) at *version* on the pipeline's branches.
Uses the local workspace clones (fetch branch → push tag ref), so git
auth (LPB_GITHUB_TOKEN / credential helper) is used and no objects need
to be transferred.
"""
if version is None:
version = get_version()
if not VERSION_RE.match(version):
cons.error(f"invalid version: {version!r} (expected 0.x.y-lpb[-dev])")
return 1
# Best-effort: warn when the local devstack VERSION is not what's committed.
root = _find_version_file()
if root is not None:
out, _, code = git(root.parent, "show", "HEAD:VERSION")
if code == 0 and out.strip() and out.strip() != version:
cons.warn(f"committed devstack VERSION is {out.strip()} (tagging {version}) — "
"run 'lpb-devstack bump' first for a normal release")
cons.info(f"Tagging 5 repos at {version} (pipeline: {pipeline})")
if dry_run:
cons.info("(dry run — nothing will be pushed)")
cons.info("")
failures = 0
for name, dev_b, main_b in TAG_REPOS:
# Branch comes from the TAG_REPOS entry (mirrors the CI tag-repos
# job); do not re-derive via expected_branch() — it would ignore
# the actual tagging set and fail for repos no longer in the stack.
b = dev_b if pipeline == "dev" else main_b
path = _resolve_repo_path(name, cons)
if path is None:
cons.error(f" {name}: no local clone at {_tag_repo_hint(name)} — "
f"run 'lpb-devstack workspace sync' / 'lpb-config update' first")
return 1
out, err, code = git_auth(path, "fetch", "--prune", "origin", "--quiet", timeout=180)
if code != 0:
cons.error(f" {name}: fetch failed ({(err.strip() or out.strip())[:80]})")
return 1
sha_out, _, sha_code = git(path, "rev-parse", f"origin/{b}")
sha = sha_out.strip()
if sha_code != 0 or not sha:
cons.error(f" {name}: branch '{b}' not found on origin — aborting (fail fast)")
return 1
existing, _, _ = git_auth(path, "ls-remote", "origin", f"refs/tags/{version}", timeout=60)
if existing.strip():
cons.info(f" {name}: {version} already exists — skipped")
continue
if dry_run:
cons.info(f" {name}: would tag {b}@{sha[:8]} → {version}")
continue
out, err, code = git_auth(path, "push", "origin", f"{sha}:refs/tags/{version}", timeout=120)
if code == 0:
cons.info(f" ✅ {name}@{version} ({b}@{sha[:8]})")
else:
cons.error(f" ❌ {name}: tag push failed ({(err.strip() or out.strip())[:120]})")
failures += 1
if dry_run:
cons.info("")
cons.info("Dry run — nothing was changed.")
return 0
if failures:
cons.error(f"{failures} repo(s) failed — re-run to complete tagging "
"(already-tagged repos are skipped)")
return 1
cons.done(f"All 5 repos tagged at {version}")
return 0
# ─── Pre-commit validation ─────────────────────────────────────────────────
def _find_devstack_root() -> Path | None:
"""Locate the devstack repo root (cwd first, then known locations)."""
cwd = Path.cwd()
for p in [cwd, *cwd.parents]:
if (p / ".githooks" / "pre-commit").is_file() and (p / "VERSION").is_file():
return p
for c in _devstack_root_candidates():
if (c / ".githooks" / "pre-commit").is_file():
return c
return None
def cmd_validate_hooks(cons: Console) -> int:
"""Run the full devstack pre-commit checks (VERSION, pins, cleanliness, tests)."""
root = _find_devstack_root()
if root is None:
cons.error("devstack repo with .githooks/pre-commit not found")
cons.info("Run this from inside the devstack workspace, or set LPB_WORKSPACE_ROOT.")
return 1
hook = root / ".githooks" / "pre-commit"
cons.info(f"Running pre-commit checks: {hook}")
cons.info("(includes the full test suite — may take a minute)")
out, err, code = run_cmd(["bash", str(hook)], cwd=str(root), timeout=1800)
if out.strip():
cons.raw(out.rstrip())
if err.strip():
cons.raw(err.rstrip())
if code != 0:
cons.error("pre-commit checks FAILED (exit %d)" % code)
return 1
cons.done("pre-commit checks passed")
return 0
# ─── CLI ──────────────────────────────────────────────────────────────────
def _add_subparser(sub, name: str, help_: str) -> argparse.ArgumentParser:
p = sub.add_parser(name, help=help_)
return p
def main(argv: list[str] | None = None) -> int:
install_sigpipe_handler()
parser = argparse.ArgumentParser(
prog="lpb-devstack",
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Environment:\n"
" LPB_WORKSPACE_ROOT Workspace root (default: /home/lpb/workspace)\n"
" LPB_DEVSTACK_DIR devstack checkout (same as --devstack-dir)\n"
" AGENT_DIR Config repo path (default: /home/lpb/.pi/agent)\n"
" LPB_AGENT_GIT Extension clone dir (default: $AGENT_DIR/git/github.com/lpb-stack)\n"
" LPB_STACK_REMOTE_BASE Stack remote base (default: https://github.com/lpb-stack)\n"
" LPB_GITHUB_TOKEN GitHub token for authenticated git operations\n\n"
"Pipeline:\n"
" Detected from VERSION file or LPB_IMAGE_TAG env var.\n"
" Override with --tag dev|main on any command."
),
)
parser.add_argument(
"--tag", default=None,
help="Override pipeline detection (dev|main)",
)
parser.add_argument(
"--devstack-dir", dest="devstack_dir", metavar="PATH", default=None,
help="devstack checkout to operate on (default: detected under "
"$LPB_WORKSPACE_ROOT by content, any directory name)",
)
sub = parser.add_subparsers(dest="command")
# VERSION bumping
p_bump = _add_subparser(sub, "bump", "Bump devstack/VERSION and commit")
p_bump.add_argument("--minor", action="store_true", help="bump minor (0.0.9 → 0.1.0)")
p_bump.add_argument("--major", action="store_true", help="bump major (0.9.9 → 1.0.0)")
p_bump.add_argument("--set", dest="set_version", metavar="V",
help="explicit version (0.x.y-lpb[-dev])")
p_bump.add_argument("--no-commit", action="store_true",
help="write VERSION but don't commit")
p_bump.add_argument("--push", action="store_true",
help="commit + push (triggers the CI build + tag)")
p_bump.add_argument("--force", action="store_true",
help="skip the dev release gate (other repos pushed/committed check)")
# Repo tagging
p_tag = _add_subparser(sub, "tag-repos", "Tag the 5 stack repos at the committed VERSION")
p_tag.add_argument("--branch", choices=["dev", "main"],
help="tag on dev branches (dev) or stable branches (main)")
p_tag.add_argument("--version", dest="version", metavar="V",
help="explicit version (default: devstack VERSION file)")
p_tag.add_argument("--dry-run", action="store_true",
help="show the plan, change nothing")
# Workspace management
p_ws = sub.add_parser("workspace", help="Manage workspace repos")
ws_sub = p_ws.add_subparsers(dest="workspace_command")
_add_subparser(ws_sub, "status", "Show workspace repo branches + alignment")
_add_subparser(ws_sub, "sync", "Prepare workspace: clone missing repos, create symlinks, align branches, pull latest")
# Stack validation
_add_subparser(sub, "validate", "Full stack alignment check (repos, branches, pins, env)")
# Release promotion
p_rel = sub.add_parser("release", help="Stable release: promote dev branches to stable")
rel_sub = p_rel.add_subparsers(dest="release_command")
_add_subparser(rel_sub, "status", "Show stable-release readiness across all repos + docs flag")
p_rel_docs = _add_subparser(rel_sub, "docs-ready",
"Flag the docs branch as reviewed for the next stable release")
p_rel_docs.add_argument("--yes", action="store_true",
help="skip the review confirmation")
p_rel_promote = _add_subparser(rel_sub, "promote",
"Merge dev branches into stable branches + push")
p_rel_promote.add_argument("--yes", action="store_true", help="skip confirmation")
p_rel_promote.add_argument("--dry-run", action="store_true",
help="show plan + feasibility, change nothing")
p_rel_promote.add_argument("--rebase", action="store_true",
help="first-release mode: for repos with unrelated "
"histories, replace stable with dev history "
"(force-push)")
p_rel_promote.add_argument("--force", action="store_true",
help="promote even if docs are not flagged ready")
# Pre-commit validation
_add_subparser(sub, "validate-hooks", "Run the full devstack pre-commit checks (tests included)")
args = parser.parse_args(argv)
cons = Console()
# Pin the devstack checkout before anything reads VERSION/stack env.
set_devstack_dir(args.devstack_dir)
if args.tag is not None and args.tag not in ("dev", "main"):
cons.error(f"invalid --tag: {args.tag} (expected dev|main)")
return 1
pipeline = detect_pipeline(args.tag)
if args.command == "bump":
return cmd_bump(cons, minor=getattr(args, "minor", False),
major=getattr(args, "major", False),
set_version=getattr(args, "set_version", None),
no_commit=getattr(args, "no_commit", False),
push=getattr(args, "push", False),
force=getattr(args, "force", False))
if args.command == "tag-repos":
return cmd_tag_repos(cons, pipeline=pipeline,
version=getattr(args, "version", None),
dry_run=getattr(args, "dry_run", False))
if args.command == "workspace":
if not args.workspace_command:
p_ws.print_help()
return 1
if args.workspace_command == "status":
return cmd_workspace_status(pipeline, cons)
if args.workspace_command == "sync":
return cmd_workspace_sync(pipeline, cons)
if args.command == "validate":
return cmd_validate(pipeline, cons)
if args.command == "release":
if not args.release_command:
p_rel.print_help()
return 1
if args.release_command == "status":
return cmd_release_status(cons)
if args.release_command == "docs-ready":
return cmd_release_docs_ready(assume_yes=getattr(args, "yes", False),
cons=cons)
if args.release_command == "promote":
return cmd_release_promote(assume_yes=getattr(args, "yes", False),
dry_run=getattr(args, "dry_run", False),
rebase=getattr(args, "rebase", False),
force=getattr(args, "force", False),
cons=cons)
if args.command == "validate-hooks":
return cmd_validate_hooks(cons)
parser.print_help()
return 1
if __name__ == "__main__":
raise SystemExit(main())
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.