From 8025cc83f06a0df67f9766dbaf96aea2707dd897 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 21 May 2026 15:00:53 -0700 Subject: [PATCH 1/4] Add 'tc-branch-cleanup' skill for canceling TeamCity builds --- .claude/scripts/tc_branch_builds.py | 88 +++++++++++++++++++++++ .claude/skills/tc-branch-cleanup/skill.md | 78 ++++++++++++++++++++ CLAUDE.md | 36 ++++++++-- 3 files changed, 197 insertions(+), 5 deletions(-) create mode 100755 .claude/scripts/tc_branch_builds.py create mode 100644 .claude/skills/tc-branch-cleanup/skill.md diff --git a/.claude/scripts/tc_branch_builds.py b/.claude/scripts/tc_branch_builds.py new file mode 100755 index 0000000000..1f5d0a40ee --- /dev/null +++ b/.claude/scripts/tc_branch_builds.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""List queued and running TeamCity builds for a given branch.""" + +import argparse +import json +import subprocess +import sys + + +def tc_api_all(endpoint: str) -> list[dict]: + """Fetch all pages from a REST endpoint using --paginate --slurp.""" + result = subprocess.run( + ["teamcity", "api", endpoint, "--paginate", "--slurp"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"Error: {result.stderr.strip()}", file=sys.stderr) + sys.exit(1) + data = json.loads(result.stdout) + # --slurp returns a flat list of objects + return data if isinstance(data, list) else [] + + +def list_queued(branch: str) -> list[dict]: + builds = tc_api_all("/app/rest/buildQueue?fields=build(id,state,branchName,buildTypeId,webUrl,buildType(name,projectName))") + return [b for b in builds if (b.get("branchName") or "") == branch] + + +def list_running(branch: str) -> list[dict]: + builds = tc_api_all("/app/rest/builds?locator=running:true&fields=build(id,state,branchName,buildTypeId,webUrl,percentageComplete,buildType(name,projectName))") + return [b for b in builds if (b.get("branchName") or "") == branch] + + +def print_builds(builds: list[dict], title: str) -> None: + print(f"\n{'='*60}") + print(f" {title} ({len(builds)})") + print(f"{'='*60}") + if not builds: + print(" (none)") + else: + for b in builds: + bt = b.get("buildType", {}) + print(f" ID : {b['id']}") + print(f" Job : {bt.get('name', b.get('buildTypeId', '?'))}") + print(f" Project : {bt.get('projectName', '?')}") + print(f" Branch : {b.get('branchName', '?')}") + if b.get("percentageComplete") is not None: + print(f" Progress: {b['percentageComplete']}%") + print(f" URL : {b.get('webUrl', '?')}") + print() + + +def main() -> None: + parser = argparse.ArgumentParser( + description="List queued and running TeamCity builds for a branch." + ) + parser.add_argument("branch", help="Exact branch name to filter by") + state_group = parser.add_mutually_exclusive_group() + state_group.add_argument("--queued-only", action="store_true", help="Only show queued builds") + state_group.add_argument("--running-only", action="store_true", help="Only show running builds") + fmt_group = parser.add_mutually_exclusive_group() + fmt_group.add_argument("--json", action="store_true", dest="as_json", help="Output raw JSON") + fmt_group.add_argument("--ids", action="store_true", help="Output build IDs only, one per line") + args = parser.parse_args() + + show_queued = not args.running_only + show_running = not args.queued_only + + queued = list_queued(args.branch) if show_queued else [] + running = list_running(args.branch) if show_running else [] + + if args.ids: + for b in queued + running: + print(b["id"]) + elif args.as_json: + print(json.dumps({"queued": queued, "running": running}, indent=2)) + else: + print(f"\nBranch filter: {args.branch!r}") + if show_queued: + print_builds(queued, "Queued Builds") + if show_running: + print_builds(running, "Running Builds") + print(f"\nTotal: {len(queued) + len(running)} build(s)") + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/tc-branch-cleanup/skill.md b/.claude/skills/tc-branch-cleanup/skill.md new file mode 100644 index 0000000000..ca93fb067e --- /dev/null +++ b/.claude/skills/tc-branch-cleanup/skill.md @@ -0,0 +1,78 @@ +--- +name: tc-branch-cleanup +description: "Cancel all queued and running TeamCity builds for a branch. Intended for post-merge cleanup. Invoke as `/tc-branch-cleanup ` with optional --comment." +--- + +# TC Branch Cleanup + +## Intent + +Use this skill when the user wants to cancel all queued and running TeamCity builds for a branch — typically after the branch has been merged and those builds are no longer relevant. + +**This is a destructive, irreversible action.** Always show the user what will be canceled and require explicit confirmation before proceeding. + +## Argument Parsing + +- **branch** (required positional) — the exact branch name to cancel builds for +- **--comment** (optional) — cancellation message stored on each build in the TeamCity UI (default: `"Branch merged — canceling remaining builds"`) + +If no branch name is provided, ask the user for one before proceeding. + +## Execution Steps + +### Step 1 — Discover builds + +Run the discovery script to find all queued and running builds for the branch: + +```bash +python3 .claude/scripts/tc_branch_builds.py --json +``` + +Parse the JSON output. It has the shape: +```json +{ + "queued": [ { "id": "...", "buildType": { "name": "...", "projectName": "..." }, ... } ], + "running": [ { "id": "...", "buildType": { "name": "...", "projectName": "..." }, ... } ] +} +``` + +### Step 2 — Confirm with user + +If there are no builds (both lists empty), report that and stop — nothing to do. + +Otherwise, display a summary table like: + +``` +Found N build(s) to cancel for branch '': + + STATE ID PROJECT / JOB + queued 12345 MyProject / Build & Test + running 12346 MyProject / Deploy + ... + +This will cancel all of the above. Proceed? (yes/no) +``` + +**Do not proceed until the user explicitly confirms.** If they decline, stop. + +### Step 3 — Cancel all builds + +For each build ID (queued and running alike), run: + +```bash +teamcity run cancel --yes --comment "" +``` + +Run these sequentially, reporting success or failure for each one as you go. + +### Step 4 — Summary + +After all cancellations are attempted, report: +- How many succeeded +- Any that failed (with the error), so the user can investigate + +## Error Handling + +- If the discovery script exits non-zero, show the error and stop before asking for confirmation. +- If an individual `teamcity run cancel` fails, log the failure and continue with the remaining builds — do not abort the whole operation. +- If the `teamcity` CLI is not on PATH, tell the user to install it or check their PATH. diff --git a/CLAUDE.md b/CLAUDE.md index 3960577764..06ca1fe20f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,13 +32,13 @@ LabKey Server is a large Java web application platform for biomedical research d ./gradlew -PmoduleSet=distributions :distributions:base:dist ``` -## Running Tests +## Tests **Unit tests** are static `TestCase` inner classes within production source files. They are registered via the module's `getUnitTests()` method and run within the server JVM. **Integration tests** require a running server and database. They are registered via `getIntegrationTests()`. -**Selenium UI tests** (in `server/testAutomation/`): +**Selenium UI tests** (in `server/testAutomation/` and the `test` directory of many modules): ```bash ./gradlew :server:testAutomation:initProperties # Generate test.properties ./gradlew :server:testAutomation:uiTests -Psuite=DRT # Run a test suite @@ -108,8 +108,15 @@ All external library versions are centralized in `gradle.properties` (200+ versi ```java private static final Logger LOG = LogHelper.getLogger(MyClass.class, "optional description"); ``` -- **Unit tests**: Create a static `TestCase` inner class extending `Assert` in the same file as production code. Use JUnit 4 annotations (`@Test`). Register new test classes in the owning module's `getUnitTests()`. -- **Selenium tests**: Subclass `BaseWebDriverTest`. Use a `@BeforeClass` for setup and override `doCleanup()` for cleanup. See `SecurityTest` as an example. +- **Unit tests**: Create a static `TestCase` inner class extending `Assert` in the same file as production code. Use JUnit 4 annotations (`@Test`). Register new test classes in the owning module's `getUnitTests()` (or `getIntegrationTests()` if the test requires the server to be running). +- **Selenium tests** + - Subclass `BaseWebDriverTest`. Use a `@BeforeClass` for setup and override `doCleanup()` for cleanup. See `SecurityTest` as an example. + - Templates for Selenium test classes and page objects are in '.idea/fileTemplates/' + - **Page/component objects over raw locators**: When a `DataRegionTable`, `CustomizeView`, or other component method returns a typed page or component object, use that object's API rather than falling back to `setFormElement`/`Locator` calls. For example, `DataRegionTable.clickInsertNewRow()` returns an insert-row page object whose fields should be set through its typed methods. + - **Remote API library over UI for setup**: When setting up a project for testing, use classes from `org.labkey.remoteapi` for setup rather than navigating through the UI. Create test-specific API wrappers for actions that are not yet exposed in the `labkey-api-java` library. + - **Use API helpers over raw Commands**: Helpers such as `org.labkey.test.params.assay.AssayDesign` and `org.labkey.test.params.experiment.SampleTypeDefinition` wrap multiple API calls into a single operation or add additional functionality. + - **Never navigate in 'finally' blocks or JUnit '@After'/'@AfterClass' methods**: It prevents the base class from collecting failure screenshots. These sorts of cleanup methods should exclusively use API calls. + - Take screenshots of errors collected by `DeferredErrorCollector` before taking any actions that modify the page state. - **Formatting**: Follow IntelliJ IDEA project settings in `.idea/codeStyles/Project.xml`. ## Key Build Properties (`gradle.properties`) @@ -126,4 +133,23 @@ When searching for Java method usages, always include `*.jsp` and `*.jspf` files ## Pull Request Format -PRs should include sections for: **Rationale** (why the change is needed), **Related Pull Requests**, and **Changes** (notable items). \ No newline at end of file +PRs should include sections for: **Rationale** (why the change is needed), **Related Pull Requests**, and **Changes** (notable items). + +## Enlistment Structure +``` +./ ← root repo (https://github.com/LabKey/server) +├── distributions/ ← optional; distribution configurations +├── remoteapi/ ← optional; Java API repos +│ ├── labkey-api-java/ ← LabKey Java Client API +│ └── labkey-api-jdbc/ ← LabKey JDBC Driver +├── server/ +│ ├── modules/ +│ │ ├── platform/ ← core platform modules +│ │ └── */ ← additional module repos cloned here +│ └── testAutomation/ ← core Selenium tests +└── clientAPIs/ ← optional; front-end packages cloned here or via env vars + ├── labkey-api-js/ + ├── labkey-ui-components/ ← $LABKEY_UI_COMPONENTS_HOME + └── labkey-ui-premium/ ← $LABKEY_UI_PREMIUM_HOME +``` +All repositories are under the `LabKey` GitHub organization (https://github.com/LabKey), with the root repo at `LabKey/server`. From 2328eaf843df732752b3b3cc918412241daaaeac Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Sun, 24 May 2026 15:45:04 -0700 Subject: [PATCH 2/4] Help getting TC CLI installed --- .claude/skills/tc-branch-cleanup/skill.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/skills/tc-branch-cleanup/skill.md b/.claude/skills/tc-branch-cleanup/skill.md index ca93fb067e..6497eedbe7 100644 --- a/.claude/skills/tc-branch-cleanup/skill.md +++ b/.claude/skills/tc-branch-cleanup/skill.md @@ -75,4 +75,4 @@ After all cancellations are attempted, report: - If the discovery script exits non-zero, show the error and stop before asking for confirmation. - If an individual `teamcity run cancel` fails, log the failure and continue with the remaining builds — do not abort the whole operation. -- If the `teamcity` CLI is not on PATH, tell the user to install it or check their PATH. +- If the `teamcity` CLI is not on PATH, tell the user to add it to their PATH or install it by following the instructions at https://www.jetbrains.com/help/teamcity/teamcity-cli.html#installing From 58fa4cdeae07fc6a1e918c42361cd55be1520f7e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 25 May 2026 09:38:23 -0700 Subject: [PATCH 3/4] Remove Scrumwise reference --- CLAUDE.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e42060d07..a5d2870cea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,7 +134,7 @@ When searching for Java method usages, always include `*.jsp` and `*.jspf` files ## Git Branch Naming - `develop` — primary development branch (protected; no direct commits). -- `fb_