Skip to content

Commit 46b74b4

Browse files
synleclaude
andcommitted
Add reusable cleanup-releases workflow, remove draft cleanup from cleanup-artifacts
Use synle/gha-workflows cleanup-releases reusable workflow for draft and incomplete release cleanup. Remove the inline draft release cleanup from cleanup-artifacts.yml to avoid duplication. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aba8a4a commit 46b74b4

5 files changed

Lines changed: 792 additions & 111 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: Cleanup Artifacts & Old Runs
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 0'
6+
workflow_dispatch:
7+
inputs:
8+
keep_last:
9+
description: 'Number of successful builds to keep artifacts from'
10+
required: false
11+
default: '1'
12+
type: number
13+
keep_runs_days:
14+
description: 'Delete runs older than N days'
15+
required: false
16+
default: '30'
17+
type: number
18+
19+
jobs:
20+
cleanup:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Cleanup artifacts and old runs
24+
uses: actions/github-script@v7
25+
with:
26+
script: |
27+
const KEEP_LAST = ${{ github.event.inputs.keep_last || 1 }};
28+
const KEEP_RUNS_DAYS = ${{ github.event.inputs.keep_runs_days || 30 }};
29+
const cutoffDate = new Date(Date.now() - KEEP_RUNS_DAYS * 24 * 60 * 60 * 1000);
30+
31+
// --- Cleanup Artifacts ---
32+
console.log(`\n=== Artifact Cleanup (keeping last ${KEEP_LAST} successful) ===\n`);
33+
34+
const runs = await github.paginate(
35+
github.rest.actions.listWorkflowRunsForRepo,
36+
{
37+
owner: context.repo.owner,
38+
repo: context.repo.repo,
39+
status: 'success',
40+
per_page: 100,
41+
}
42+
);
43+
44+
const successRunIds = new Set(runs.map(r => r.id));
45+
46+
const artifacts = await github.paginate(
47+
github.rest.actions.listArtifactsForRepo,
48+
{
49+
owner: context.repo.owner,
50+
repo: context.repo.repo,
51+
per_page: 100,
52+
}
53+
);
54+
55+
const successful = artifacts
56+
.filter(a => successRunIds.has(a.workflow_run?.id))
57+
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
58+
59+
const failed = artifacts
60+
.filter(a => !successRunIds.has(a.workflow_run?.id));
61+
62+
// Group successful artifacts by run, keep all artifacts from the last N runs
63+
const runIds = [...new Set(successful.map(a => a.workflow_run?.id))];
64+
const keepRunIds = new Set(runIds.slice(0, KEEP_LAST));
65+
66+
const toDelete = [
67+
...successful.filter(a => !keepRunIds.has(a.workflow_run?.id)),
68+
...failed,
69+
];
70+
71+
for (const artifact of toDelete) {
72+
console.log(`Deleting artifact: ${artifact.name} (run: ${artifact.workflow_run?.id}, created: ${artifact.created_at})`);
73+
await github.rest.actions.deleteArtifact({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
artifact_id: artifact.id,
77+
});
78+
}
79+
80+
console.log(`\nArtifacts — Kept runs: ${keepRunIds.size}, Deleted artifacts: ${toDelete.length}`);
81+
82+
// --- Cleanup Old Workflow Runs ---
83+
console.log(`\n=== Workflow Run Cleanup (older than ${KEEP_RUNS_DAYS} days) ===\n`);
84+
85+
const allRuns = await github.paginate(
86+
github.rest.actions.listWorkflowRunsForRepo,
87+
{
88+
owner: context.repo.owner,
89+
repo: context.repo.repo,
90+
per_page: 100,
91+
}
92+
);
93+
94+
const oldRuns = allRuns.filter(r => new Date(r.created_at) < cutoffDate);
95+
96+
for (const run of oldRuns) {
97+
console.log(`Deleting run: #${run.run_number} ${run.name} (${run.status}, created: ${run.created_at})`);
98+
await github.rest.actions.deleteWorkflowRun({
99+
owner: context.repo.owner,
100+
repo: context.repo.repo,
101+
run_id: run.id,
102+
});
103+
}
104+
105+
console.log(`\nWorkflow runs deleted: ${oldRuns.length}`);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: cleanup-releases
2+
3+
on:
4+
schedule:
5+
- cron: "0 0 * * 0"
6+
workflow_dispatch:
7+
inputs:
8+
cleanup_drafts:
9+
description: "Delete old draft releases"
10+
type: boolean
11+
default: true
12+
draft_keep_count:
13+
description: "Number of most recent draft releases to keep"
14+
type: number
15+
default: 0
16+
cleanup_incomplete:
17+
description: "Delete incomplete releases (missing assets)"
18+
type: boolean
19+
default: true
20+
expected_assets:
21+
description: "Expected number of assets per release"
22+
type: number
23+
default: 4
24+
lookback_months:
25+
description: "How many months back to check for incomplete releases"
26+
type: number
27+
default: 3
28+
dry_run:
29+
description: "Preview deletions without actually deleting"
30+
type: boolean
31+
default: true
32+
33+
concurrency:
34+
group: ${{ github.workflow }}-${{ github.ref }}
35+
cancel-in-progress: true
36+
37+
jobs:
38+
cleanup:
39+
# https://github.com/synle/gha-workflows/blob/main/.github/workflows/cleanup-releases.yml
40+
uses: synle/gha-workflows/.github/workflows/cleanup-releases.yml@main
41+
permissions:
42+
contents: write
43+
with:
44+
cleanup-drafts: ${{ inputs.cleanup_drafts || true }}
45+
draft-keep-count: ${{ inputs.draft_keep_count || 0 }}
46+
cleanup-incomplete: ${{ inputs.cleanup_incomplete || true }}
47+
expected-assets: ${{ inputs.expected_assets || 4 }}
48+
lookback-months: ${{ inputs.lookback_months || 3 }}
49+
dry-run: ${{ inputs.dry_run || false }}

CLAUDE.md

Lines changed: 1 addition & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,7 @@ Cross-platform desktop system tray application for controlling monitor brightnes
66

77
Display and dark mode operations are delegated to the [display-dj CLI](https://github.com/synle/display-dj-cli), which runs as a bundled HTTP server sidecar. The Tauri backend makes HTTP requests to it. Volume control remains platform-specific in Rust.
88

9-
## Architecture
10-
11-
### Frontend (`src/`)
12-
13-
- React 18 + TypeScript, bundled with Vite
14-
- Communicates with backend via `invoke()` from `@tauri-apps/api/core`
15-
- Listens for backend events via `listen()` from `@tauri-apps/api/event`
16-
- Components: Header, Slider, MonitorControl, AllMonitorsControl, VolumeControl, DarkModeToggle, SettingsPanel
17-
- `types.ts` — shared TypeScript interfaces (Monitor, MonitorMetadata, Preferences, KeyBinding, NightModeSchedule, Profile)
18-
- `types.d.ts` — global type definitions (Command, DisplayType, BrightnessPreset, etc.)
19-
- `constants.ts` — shared constants (e.g. `LAPTOP_BUILT_IN_DISPLAY_ID`)
20-
21-
### Backend (`src-tauri/src/`)
22-
23-
- `lib.rs` — Tauri app setup, plugin init, sidecar launch (display-dj HTTP server), port discovery, window management, dock hiding (macOS), night mode schedule checker
24-
- `display.rs` — Monitor brightness and contrast via HTTP requests to the display-dj server
25-
- `dark_mode.rs` — Dark mode detection and toggling via HTTP requests to the display-dj server
26-
- `volume.rs` — System volume get/set (platform-specific, not via display-dj)
27-
- `config.rs` — Preferences persistence (JSON file in OS config dir, includes per-monitor metadata), night mode schedule, min brightness with absolute floor, migration from legacy `monitor-configs.json`, reset to defaults
28-
- `tray.rs` — System tray menu, window positioning, global keyboard shortcuts
29-
30-
### display-dj CLI sidecar (`src-tauri/binaries/`)
31-
32-
The [display-dj CLI](https://github.com/synle/display-dj-cli) is bundled as a Tauri sidecar. On app startup, `lib.rs` finds an available port (starting from 51337) and spawns `display-dj-server serve <port>`. All display and dark mode operations go through its HTTP API at `http://127.0.0.1:<port>/`.
33-
34-
Key HTTP routes used:
35-
36-
- `GET /get_all` — list all displays with live brightness and contrast
37-
- `GET /set_one/<id>/<level>` — set one display's brightness
38-
- `GET /set_all/<level>` — set all displays' brightness
39-
- `GET /set_contrast_one/<id>/<level>` — set one display's contrast (DDC-only, 0-100)
40-
- `GET /set_contrast_all/<level>` — set all displays' contrast (DDC-only, 0-100)
41-
- `GET /dark` / `GET /light` — switch dark/light mode
42-
- `GET /theme` — get current theme
43-
- `GET /health` — server health check
44-
- `GET /debug` — full diagnostics: version, OS/arch, display enumeration, active tests (brightness/contrast per display, volume, theme). Restores all settings after testing. Returns JSON.
45-
46-
**Sidecar lifecycle:** The `CommandChild` handle is stored in `AppState.sidecar_child`. On app exit, the `RunEvent::Exit` handler in `lib.rs::run()` calls `child.kill()` to terminate the sidecar server. This prevents orphaned `display-dj-server` processes after the main app closes.
47-
48-
Sidecar binaries follow Tauri's naming convention:
49-
50-
```
51-
src-tauri/binaries/
52-
display-dj-server-aarch64-apple-darwin # macOS ARM
53-
display-dj-server-x86_64-apple-darwin # macOS Intel
54-
display-dj-server-x86_64-pc-windows-msvc.exe # Windows x64
55-
display-dj-server-x86_64-unknown-linux-gnu # Linux x64
56-
```
57-
58-
### Volume (platform-specific)
59-
60-
Volume is the only module with platform-specific code, as the display-dj CLI does not handle volume:
61-
62-
- **macOS**: `osascript` (CoreAudio)
63-
- **Windows**: PowerShell + WASAPI COM interop
64-
- **Linux**: `pactl` (PulseAudio/PipeWire)
9+
For full architecture details, request lifecycle, layer-by-layer breakdown, data flow diagrams, and "where to edit" reference, see **[DEV.md](DEV.md)**.
6510

6611
## Build Commands
6712

@@ -100,23 +45,6 @@ cd src-tauri && cargo test # Run all Rust backend tests
10045

10146
GitHub Actions (`build.yml`) runs `npm test` and `cargo test` on all platforms (macOS ARM/Intel, Windows, Linux) for every push and PR.
10247

103-
## Config File Locations
104-
105-
- **macOS**: `~/Library/Application Support/display-dj/`
106-
- **Windows**: `%APPDATA%/display-dj/`
107-
- **Linux**: `~/.config/display-dj/`
108-
109-
Files: `preferences.json` (includes per-monitor metadata — labels, sort order — as `monitorConfigs` array)
110-
111-
## Monitor Identity (UID scheme)
112-
113-
Each monitor is identified by a composite UID: `{api_id}::{api_model_name}` (e.g. `"1::Dell U2723QE"`, `"builtin::Built-in Display"`). This is more stable than the raw integer ID from the sidecar API, which can collide when monitors are swapped.
114-
115-
- `Monitor.id` — raw API id, used for sidecar HTTP calls (`/set_one/{id}/{value}`)
116-
- `Monitor.uid` — composite key, used for config lookups, React keys, rename/reorder operations
117-
- `MonitorMetadata` entries in `preferences.monitorConfigs` are **append-only** — new monitors are added on first detection, never removed on unplug. This preserves labels and sort order across plug/unplug cycles.
118-
- On startup, a one-time migration converts old `monitor-configs.json` entries into `MonitorMetadata` format within preferences.
119-
12048
## Formatting
12149

12250
After making changes to frontend code (`src/`), config files, or docs, always run `npx prettier --write` on the changed files before considering the task done. The prettier hook in `.claude/settings.json` handles this automatically for Edit/Write tool calls, but if you create or modify files via other means, run prettier manually.
@@ -139,43 +67,6 @@ After making changes to frontend code (`src/`), config files, or docs, always ru
13967
- Brightness values are clamped to `effective_min_brightness()` which enforces an absolute floor of 5
14068
- Contrast is DDC-only (`Option<u32>` / `number | null`): built-in displays return `null`. The contrast slider is hidden by default and toggled via the `showContrast` preference in Settings
14169

142-
## Window Positioning (multi-monitor DPI)
143-
144-
The tray popup window must appear next to the tray icon, which can be on any monitor with any DPI scale factor. This is deceptively hard because of how Tauri handles coordinates on macOS. **Read the doc comment on `position_window_near_tray` in `tray.rs` before modifying positioning code.**
145-
146-
### The coordinate spaces
147-
148-
| API | Returns | Coordinate space |
149-
| --------------------------------------- | ----------------------------------- | ------------------------------------------------------------ |
150-
| `tray.rect()` | `PhysicalPosition` / `PhysicalSize` | Global physical pixels |
151-
| `monitor.position()` / `monitor.size()` | `PhysicalPosition` / `PhysicalSize` | Global physical pixels |
152-
| `window.set_position(PhysicalPosition)` || Tauri divides by `window.scale_factor()` to get macOS points |
153-
| `window.scale_factor()` | `f64` | Scale of the monitor the window is **currently** on |
154-
155-
### The pitfall
156-
157-
`window.scale_factor()` reflects the **current** monitor, not the target. When the window is on a 1× display and the tray is clicked on a 2× display, Tauri's `set_position` divides by 1 instead of 2, placing the window at double the intended macOS-point coordinate (off-screen).
158-
159-
**Attempted fix that does NOT work:** moving the hidden window to the target monitor before positioning. `scale_factor()` does not update synchronously after `set_position`.
160-
161-
### The fix: scale compensation
162-
163-
All positioning math runs in the global physical pixel space using `target_scale` (the tray's monitor). Before calling `set_position`, multiply by `window_scale / target_scale`:
164-
165-
```
166-
Tauri does: point = arg / window_scale
167-
We need: point = physical / target_scale
168-
So we pass: arg = physical × window_scale / target_scale
169-
```
170-
171-
When both scales match (same monitor), the compensation is 1× (no-op).
172-
173-
### Debug logging
174-
175-
Enable debug logging via the tray menu → "Debug" → "Enable Logging" to write positioning data to `debug.log` in the config directory (auto-truncated at 1 MB, keeps last 80% when limit is hit). Open via tray menu → "Debug" → "Open Debug Log". Each tray click logs: tray rect, all monitors (position/size/scale), target selection, window scale, computed position, compensation factor, and final `set_position` arguments.
176-
177-
When debug logging is enabled, the app also calls the sidecar's `/debug` endpoint on startup and prepends the full diagnostic dump (version, OS, display enumeration, active brightness/contrast/volume/theme tests) to the debug log. This is useful for troubleshooting display detection issues. You can also hit the endpoint directly: `curl http://127.0.0.1:<port>/debug`.
178-
17970
## Related Projects
18071

18172
- **[display-dj-cli](https://github.com/synle/display-dj-cli)** — The Rust CLI/HTTP server that handles all display operations (brightness, contrast, dark mode). Bundled as a Tauri sidecar. Source at `/Users/syle/Downloads/display-dj-cli`. When bumping the sidecar version, always review upstream changes in that repo.

0 commit comments

Comments
 (0)