-
Notifications
You must be signed in to change notification settings - Fork 350
feat: add cleanup-cache-memory job to agentics maintenance workflow #25908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
5c41afb
ed5d105
56107ff
c53967e
5a6ae0e
a83d229
600d185
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,335 @@ | ||||||||||||||||||
| // @ts-check | ||||||||||||||||||
| /// <reference types="@actions/github-script" /> | ||||||||||||||||||
|
|
||||||||||||||||||
| const { getErrorMessage } = require("./error_helpers.cjs"); | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Delay execution for a given number of milliseconds. | ||||||||||||||||||
| * Used to avoid GitHub API throttling between requests. | ||||||||||||||||||
| * @param {number} ms - Milliseconds to wait | ||||||||||||||||||
| * @returns {Promise<void>} | ||||||||||||||||||
| */ | ||||||||||||||||||
| function delay(ms) { | ||||||||||||||||||
| return new Promise(resolve => setTimeout(resolve, ms)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Check the current rate limit and determine if we should continue. | ||||||||||||||||||
| * Returns the remaining requests count, or -1 if we couldn't check. | ||||||||||||||||||
| * @param {any} github - GitHub REST client | ||||||||||||||||||
| * @returns {Promise<number>} Remaining requests, or -1 on error | ||||||||||||||||||
| */ | ||||||||||||||||||
| async function getRateLimitRemaining(github) { | ||||||||||||||||||
|
||||||||||||||||||
| try { | ||||||||||||||||||
| const { data } = await github.rest.rateLimit.get(); | ||||||||||||||||||
| return data.rate.remaining; | ||||||||||||||||||
| } catch { | ||||||||||||||||||
| return -1; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Minimum rate limit remaining before we skip further operations. | ||||||||||||||||||
| * This reserves capacity for other workflow jobs and API consumers. | ||||||||||||||||||
| */ | ||||||||||||||||||
| const MIN_RATE_LIMIT_REMAINING = 100; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Default delay in ms between delete operations to avoid throttling. | ||||||||||||||||||
| */ | ||||||||||||||||||
| const DELETE_DELAY_MS = 250; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Default delay in ms between list pages to avoid throttling. | ||||||||||||||||||
| */ | ||||||||||||||||||
| const LIST_DELAY_MS = 100; | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Extract the run ID from a cache key. | ||||||||||||||||||
| * Cache keys follow the pattern: memory-{parts}-{runID} | ||||||||||||||||||
| * where runID is the last numeric segment. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param {string} key - Cache key string | ||||||||||||||||||
| * @returns {number | null} The extracted run ID, or null if not found | ||||||||||||||||||
| */ | ||||||||||||||||||
| function extractRunId(key) { | ||||||||||||||||||
|
||||||||||||||||||
| const parts = key.split("-"); | ||||||||||||||||||
| // Walk backwards to find the last purely numeric segment | ||||||||||||||||||
| for (let i = parts.length - 1; i >= 0; i--) { | ||||||||||||||||||
| if (/^\d+$/.test(parts[i])) { | ||||||||||||||||||
| return parseInt(parts[i], 10); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| return null; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Derive the group key from a cache key by removing the run ID suffix. | ||||||||||||||||||
| * This groups caches that differ only by their run ID. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param {string} key - Cache key string | ||||||||||||||||||
| * @returns {string} The group key (everything before the run ID) | ||||||||||||||||||
| */ | ||||||||||||||||||
| function deriveGroupKey(key) { | ||||||||||||||||||
| const parts = key.split("-"); | ||||||||||||||||||
| // Walk backwards to find the last purely numeric segment and strip it | ||||||||||||||||||
| for (let i = parts.length - 1; i >= 0; i--) { | ||||||||||||||||||
| if (/^\d+$/.test(parts[i])) { | ||||||||||||||||||
| return parts.slice(0, i).join("-"); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| // If no numeric segment found, return the full key | ||||||||||||||||||
| return key; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * @typedef {Object} CacheEntry | ||||||||||||||||||
| * @property {number} id - Cache ID for deletion | ||||||||||||||||||
| * @property {string} key - Full cache key | ||||||||||||||||||
| * @property {number | null} runId - Extracted run ID | ||||||||||||||||||
| * @property {string} groupKey - Group key (key without run ID) | ||||||||||||||||||
| */ | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * List all caches starting with "memory-" prefix, handling pagination. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param {any} github - GitHub REST client | ||||||||||||||||||
| * @param {string} owner - Repository owner | ||||||||||||||||||
| * @param {string} repo - Repository name | ||||||||||||||||||
| * @param {number} [listDelayMs] - Delay between list pages in ms | ||||||||||||||||||
| * @returns {Promise<CacheEntry[]>} List of cache entries | ||||||||||||||||||
| */ | ||||||||||||||||||
| async function listMemoryCaches(github, owner, repo, listDelayMs = LIST_DELAY_MS) { | ||||||||||||||||||
| /** @type {CacheEntry[]} */ | ||||||||||||||||||
| const caches = []; | ||||||||||||||||||
| let page = 1; | ||||||||||||||||||
| const perPage = 100; | ||||||||||||||||||
|
|
||||||||||||||||||
| while (true) { | ||||||||||||||||||
|
||||||||||||||||||
| const response = await github.rest.actions.getActionsCacheList({ | ||||||||||||||||||
| owner, | ||||||||||||||||||
| repo, | ||||||||||||||||||
| key: "memory-", | ||||||||||||||||||
| per_page: perPage, | ||||||||||||||||||
| page, | ||||||||||||||||||
| sort: "key", | ||||||||||||||||||
|
||||||||||||||||||
| direction: "asc", | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| const actionsCaches = response.data.actions_caches; | ||||||||||||||||||
| if (!actionsCaches || actionsCaches.length === 0) { | ||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| for (const cache of actionsCaches) { | ||||||||||||||||||
| if (!cache.key || !cache.key.startsWith("memory-")) { | ||||||||||||||||||
| continue; | ||||||||||||||||||
| } | ||||||||||||||||||
| caches.push({ | ||||||||||||||||||
| id: cache.id, | ||||||||||||||||||
| key: cache.key, | ||||||||||||||||||
| runId: extractRunId(cache.key), | ||||||||||||||||||
| groupKey: deriveGroupKey(cache.key), | ||||||||||||||||||
| }); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (actionsCaches.length < perPage) { | ||||||||||||||||||
| break; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| page++; | ||||||||||||||||||
| // Throttle between list pages | ||||||||||||||||||
| await delay(listDelayMs); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return caches; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Group caches by their group key (everything except run ID), | ||||||||||||||||||
| * then for each group keep only the entry with the highest run ID | ||||||||||||||||||
| * and return the rest for deletion. | ||||||||||||||||||
| * | ||||||||||||||||||
| * @param {CacheEntry[]} caches - List of cache entries | ||||||||||||||||||
| * @returns {{ toDelete: CacheEntry[], kept: CacheEntry[] }} | ||||||||||||||||||
| */ | ||||||||||||||||||
| function identifyCachesToDelete(caches) { | ||||||||||||||||||
| /** @type {Map<string, CacheEntry[]>} */ | ||||||||||||||||||
| const groups = new Map(); | ||||||||||||||||||
|
|
||||||||||||||||||
| for (const cache of caches) { | ||||||||||||||||||
| if (cache.runId === null) { | ||||||||||||||||||
| // Skip caches without a recognizable run ID | ||||||||||||||||||
| continue; | ||||||||||||||||||
| } | ||||||||||||||||||
| const group = groups.get(cache.groupKey) || []; | ||||||||||||||||||
| group.push(cache); | ||||||||||||||||||
| groups.set(cache.groupKey, group); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** @type {CacheEntry[]} */ | ||||||||||||||||||
| const toDelete = []; | ||||||||||||||||||
| /** @type {CacheEntry[]} */ | ||||||||||||||||||
| const kept = []; | ||||||||||||||||||
|
|
||||||||||||||||||
| for (const [, group] of groups) { | ||||||||||||||||||
| if (group.length <= 1) { | ||||||||||||||||||
| // Only one entry in this group, nothing to clean up | ||||||||||||||||||
| if (group.length === 1) { | ||||||||||||||||||
| kept.push(group[0]); | ||||||||||||||||||
| } | ||||||||||||||||||
| continue; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Sort by run ID descending (highest first = latest) | ||||||||||||||||||
| group.sort((a, b) => (b.runId ?? 0) - (a.runId ?? 0)); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Keep the first (latest), mark the rest for deletion | ||||||||||||||||||
| kept.push(group[0]); | ||||||||||||||||||
| toDelete.push(...group.slice(1)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return { toDelete, kept }; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** | ||||||||||||||||||
| * Main entry point: cleanup outdated cache-memory caches. | ||||||||||||||||||
| * | ||||||||||||||||||
| * Lists all caches with "memory-" prefix, groups them by key prefix, | ||||||||||||||||||
| * keeps the latest run ID per group, and deletes the rest. | ||||||||||||||||||
| * Includes timeouts to avoid GitHub API throttling and skips | ||||||||||||||||||
| * if rate limiting is too high. | ||||||||||||||||||
|
||||||||||||||||||
| * if rate limiting is too high. | |
| * if the remaining rate limit is too low. |
Copilot
AI
Apr 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The periodic rate-limit check is keyed off deletedCount, so if deletions fail (and deletedCount doesn’t increment) the script may never re-check rate limits and could continue making API calls even as the limit drops. Track attempts/processed count (or use the loop index) for the modulo check instead of successful deletions.
| for (const cache of toDelete) { | |
| // Check rate limit periodically (every 10 deletions) | |
| if (deletedCount > 0 && deletedCount % 10 === 0) { | |
| for (let i = 0; i < toDelete.length; i++) { | |
| const cache = toDelete[i]; | |
| // Check rate limit periodically (every 10 processed caches) | |
| if (i > 0 && i % 10 === 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot find other delay impl, refactor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored in 56107ff — now imports
delayfromexpired_entity_cleanup_helpers.cjsinstead of duplicating it.