From 28999f2b3b78518e9a99a0ecefe395bd7cd6847f Mon Sep 17 00:00:00 2001 From: mdzurick Date: Thu, 16 Apr 2026 10:57:35 +0000 Subject: [PATCH 1/5] Paginate deployment cleanup to drain all ghcr-ci entries Signed-off-by: mdzurick --- .github/workflows/clean_up.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index a5bedab7c05..10535ef11c6 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -158,13 +158,17 @@ jobs: with: script: | for (const environment of ['default', 'ghcr-ci']) { - const deployments = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - environment: environment - }); + const deployments = await github.paginate( + github.rest.repos.listDeployments, + { + owner: context.repo.owner, + repo: context.repo.repo, + environment: environment, + per_page: 100 + } + ); await Promise.all( - deployments.data.map(async (deployment) => { + deployments.map(async (deployment) => { await github.rest.repos.createDeploymentStatus({ owner: context.repo.owner, repo: context.repo.repo, From c94460f1c686b05795bfd545e729a2d3c8f9daec Mon Sep 17 00:00:00 2001 From: mdzurick Date: Thu, 16 Apr 2026 11:03:46 +0000 Subject: [PATCH 2/5] show status of deletions Signed-off-by: mdzurick --- .github/workflows/clean_up.yml | 38 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index 10535ef11c6..a0c8a48a723 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -157,6 +157,7 @@ jobs: - uses: actions/github-script@v7 with: script: | + const BATCH = 100; for (const environment of ['default', 'ghcr-ci']) { const deployments = await github.paginate( github.rest.repos.listDeployments, @@ -167,21 +168,28 @@ jobs: per_page: 100 } ); - await Promise.all( - deployments.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); + console.log(`${environment}: ${deployments.length} deployments to delete`); + let deleted = 0; + for (let i = 0; i < deployments.length; i += BATCH) { + const batch = deployments.slice(i, i + BATCH); + await Promise.all( + batch.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + deleted += batch.length; + console.log(`${environment}: deleted ${deleted}/${deployments.length}`); + } } pr_cleanup: From be9e462843c2760af72600a109b2af29f8b5330c Mon Sep 17 00:00:00 2001 From: mdzurick Date: Thu, 16 Apr 2026 11:13:23 +0000 Subject: [PATCH 3/5] Various changes to the deletion mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. github.paginate() → github.paginate.iterator() — streams one page at a time instead of loading 76K deployments into memory 2. Promise.all → Promise.allSettled — partial failures don't crash the batch; counts successes and stops when rate-limited 3. BATCH 100 → 50 — stays within GitHub's secondary rate limit (~100 concurrent API calls) 4. Fixed indentation — object properties now indented properly relative to their braces 5. response.data — iterator yields response objects, so .data is used correctly to access the deployment array per page Signed-off-by: mdzurick --- .github/workflows/clean_up.yml | 53 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index a0c8a48a723..fc1d317bfc1 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -157,9 +157,10 @@ jobs: - uses: actions/github-script@v7 with: script: | - const BATCH = 100; + const BATCH = 50; for (const environment of ['default', 'ghcr-ci']) { - const deployments = await github.paginate( + let deleted = 0; + for await (const response of github.paginate.iterator( github.rest.repos.listDeployments, { owner: context.repo.owner, @@ -167,29 +168,33 @@ jobs: environment: environment, per_page: 100 } - ); - console.log(`${environment}: ${deployments.length} deployments to delete`); - let deleted = 0; - for (let i = 0; i < deployments.length; i += BATCH) { - const batch = deployments.slice(i, i + BATCH); - await Promise.all( - batch.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); - deleted += batch.length; - console.log(`${environment}: deleted ${deleted}/${deployments.length}`); + )) { + for (let i = 0; i < response.data.length; i += BATCH) { + const batch = response.data.slice(i, i + BATCH); + const results = await Promise.allSettled( + batch.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + const fulfilled = results.filter(r => r.status === 'fulfilled').length; + deleted += fulfilled; + if (fulfilled < batch.length) { + console.log(`${environment}: ${batch.length - fulfilled} failures in batch, stopping.`); + break; + } + } } + console.log(`${environment}: deleted ${deleted} deployments this run`); } pr_cleanup: From fc3ab89b5331f0316c8cc3176d0bf3adfea6ad78 Mon Sep 17 00:00:00 2001 From: mdzurick Date: Thu, 16 Apr 2026 11:20:16 +0000 Subject: [PATCH 4/5] Remove pagination, only remove first 200 - No pagination at all. Fetches page 1 repeatedly with listDeployments (50 items per call). After deleting those, the next fetch returns the next batch since the old ones are gone. - Capped at 200 deletions per run (MAX_PER_RUN). Uses ~408 API calls per environment per run, well within the 15,000/hour budget shared with CI. - Still uses Promise.allSettled so a single 403 doesn't crash the step. Stops gracefully on partial failures. Signed-off-by: mdzurick --- .github/workflows/clean_up.yml | 56 ++++++++++++++++------------------ 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index fc1d317bfc1..6b1473461d2 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -158,40 +158,38 @@ jobs: with: script: | const BATCH = 50; + const MAX_PER_RUN = 200; for (const environment of ['default', 'ghcr-ci']) { let deleted = 0; - for await (const response of github.paginate.iterator( - github.rest.repos.listDeployments, - { + while (deleted < MAX_PER_RUN) { + const {data: deployments} = await github.rest.repos.listDeployments({ owner: context.repo.owner, repo: context.repo.repo, environment: environment, - per_page: 100 - } - )) { - for (let i = 0; i < response.data.length; i += BATCH) { - const batch = response.data.slice(i, i + BATCH); - const results = await Promise.allSettled( - batch.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); - const fulfilled = results.filter(r => r.status === 'fulfilled').length; - deleted += fulfilled; - if (fulfilled < batch.length) { - console.log(`${environment}: ${batch.length - fulfilled} failures in batch, stopping.`); - break; - } + per_page: Math.min(BATCH, MAX_PER_RUN - deleted) + }); + if (deployments.length === 0) break; + + const results = await Promise.allSettled( + deployments.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + const fulfilled = results.filter(r => r.status === 'fulfilled').length; + deleted += fulfilled; + if (fulfilled < deployments.length) { + console.log(`${environment}: ${deployments.length - fulfilled} failures, stopping.`); + break; } } console.log(`${environment}: deleted ${deleted} deployments this run`); From d1871a31dd0af31ee696f405baf1bb26ed123fb9 Mon Sep 17 00:00:00 2001 From: mdzurick Date: Thu, 16 Apr 2026 11:53:17 +0000 Subject: [PATCH 5/5] bump MAX_PER_RUN to 500 Signed-off-by: mdzurick --- .github/workflows/clean_up.yml | 65 ++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/.github/workflows/clean_up.yml b/.github/workflows/clean_up.yml index 6b1473461d2..b10651539cf 100644 --- a/.github/workflows/clean_up.yml +++ b/.github/workflows/clean_up.yml @@ -158,38 +158,43 @@ jobs: with: script: | const BATCH = 50; - const MAX_PER_RUN = 200; + const MAX_PER_RUN = 500; for (const environment of ['default', 'ghcr-ci']) { let deleted = 0; - while (deleted < MAX_PER_RUN) { - const {data: deployments} = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - environment: environment, - per_page: Math.min(BATCH, MAX_PER_RUN - deleted) - }); - if (deployments.length === 0) break; - - const results = await Promise.allSettled( - deployments.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); - const fulfilled = results.filter(r => r.status === 'fulfilled').length; - deleted += fulfilled; - if (fulfilled < deployments.length) { - console.log(`${environment}: ${deployments.length - fulfilled} failures, stopping.`); - break; + try { + while (deleted < MAX_PER_RUN) { + const {data: deployments} = await github.rest.repos.listDeployments({ + owner: context.repo.owner, + repo: context.repo.repo, + environment: environment, + per_page: Math.min(BATCH, MAX_PER_RUN - deleted) + }); + if (deployments.length === 0) break; + + const results = await Promise.allSettled( + deployments.map(async (deployment) => { + await github.rest.repos.createDeploymentStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id, + state: 'inactive' + }); + return github.rest.repos.deleteDeployment({ + owner: context.repo.owner, + repo: context.repo.repo, + deployment_id: deployment.id + }); + }) + ); + const fulfilled = results.filter(r => r.status === 'fulfilled').length; + deleted += fulfilled; + if (fulfilled < deployments.length) break; + } + } catch (e) { + if (e.status === 403) { + console.log(`${environment}: rate limited, will continue next run.`); + } else { + throw e; } } console.log(`${environment}: deleted ${deleted} deployments this run`);