From edfed0ef05a36835d5832d857ecdce94d569a3dd Mon Sep 17 00:00:00 2001 From: Rustie Lin Date: Fri, 12 Apr 2024 12:28:46 -0700 Subject: [PATCH] add action for prune-github-workflow-runs --- prune-github-workflow-runs/action.yaml | 22 ++ prune-github-workflow-runs/lib/main.js | 123 +++++++ prune-github-workflow-runs/lib/main.js.map | 1 + prune-github-workflow-runs/package.json | 19 ++ prune-github-workflow-runs/pnpm-lock.yaml | 359 +++++++++++++++++++++ prune-github-workflow-runs/src/main.ts | 126 ++++++++ prune-github-workflow-runs/tsconfig.json | 15 + 7 files changed, 665 insertions(+) create mode 100644 prune-github-workflow-runs/action.yaml create mode 100644 prune-github-workflow-runs/lib/main.js create mode 100644 prune-github-workflow-runs/lib/main.js.map create mode 100644 prune-github-workflow-runs/package.json create mode 100644 prune-github-workflow-runs/pnpm-lock.yaml create mode 100644 prune-github-workflow-runs/src/main.ts create mode 100644 prune-github-workflow-runs/tsconfig.json diff --git a/prune-github-workflow-runs/action.yaml b/prune-github-workflow-runs/action.yaml new file mode 100644 index 0000000..25da697 --- /dev/null +++ b/prune-github-workflow-runs/action.yaml @@ -0,0 +1,22 @@ +name: 'Prune Github Workflow Runs' +description: 'Delete obsolete runs of workflows that do not exist anymore in the given repo.' + +inputs: + token: + description: GitHub access token + required: true + owner: + description: Repository owner + required: false + default: ${{ github.repository_owner }} + repo: + description: Repository name + required: false + default: ${{ github.event.repository.name }} + dry-run: + description: 'Do not delete runs, just print them' + required: false + default: 'false' +runs: + using: node20 + main: 'lib/main.js' \ No newline at end of file diff --git a/prune-github-workflow-runs/lib/main.js b/prune-github-workflow-runs/lib/main.js new file mode 100644 index 0000000..57d309d --- /dev/null +++ b/prune-github-workflow-runs/lib/main.js @@ -0,0 +1,123 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pruneGithubWorkflowRuns = void 0; +const core_1 = __importDefault(require("@actions/core")); +const github_1 = __importDefault(require("@actions/github")); +const glob_1 = __importDefault(require("@actions/glob")); +const path = __importStar(require("path")); +const token = core_1.default.getInput("token", { required: true }); +const owner = core_1.default.getInput("owner", { required: false }); +const repo = core_1.default.getInput("repo", { required: false }); +const dryRun = core_1.default.getInput("dry-run") !== "false"; // any value other than "false" will be considered as true to be safe +function pruneGithubWorkflowRuns() { + return __awaiter(this, void 0, void 0, function* () { + const ghClient = github_1.default.getOctokit(token, { + throttle: { + onRateLimit: (retryAfter = 0, options) => { + console.warn(`Request quota exhausted for request ${options.method} ${options.url}`); + if (options.request.retryCount === 0) { + // only retries once + console.log(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + onAbuseLimit: (retryAfter = 0, options) => { + console.warn(`Abuse detected for request ${options.method} ${options.url}`); + if (options.request.retryCount === 0) { + // only retries once + console.log(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + }, + previews: ["ant-man"], + }); + const patterns = [`.github/worklows/*.yml`, `.github/workflows/*.yaml`]; + const globber = yield glob_1.default.create(patterns.join("\n")); + const workflowFilePaths = yield globber.glob(); + const workflowFilesPresentInRepo = workflowFilePaths.map((filePath) => path.basename(filePath)); + if (workflowFilesPresentInRepo.length === 0) { + core_1.default.setFailed("Found 0 workflow files under `.github/workflows` which is kinda odd - exiting early..."); + return; + } + core_1.default.info(`\nFound the following workflow files in the repo:\n${workflowFilesPresentInRepo.join("\n")}`); + const workflowResponse = yield ghClient.paginate(ghClient.rest.actions.listRepoWorkflows, { + owner, + repo, + }, (response) => response.data); + const obsoleteWorkflows = workflowResponse.filter((workflow) => !workflowFilesPresentInRepo.includes(path.basename(workflow.path))); + let totalDeleted = 0; + core_1.default.info(` +Found ${obsoleteWorkflows.length} obsolete workflows: +${obsoleteWorkflows.map((wf) => `'${wf.name}' - path: ${wf.path}`).join("\n")} +Deleting their workflow runs now...`); + for (const wf of obsoleteWorkflows) { + core_1.default.info("Deleting workflow runs of workflow: " + wf.name); + const workflowRuns = yield ghClient.paginate(ghClient.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id: wf.id, + }, (response) => response.data); + for (const [index, run] of workflowRuns.entries()) { + core_1.default.info(`Workflow: "${wf.name}" - Deleting Run (${index + 1}/${workflowRuns.length}) - Run ID: ${run.id}`); + try { + if (!dryRun) { + yield ghClient.rest.actions.deleteWorkflowRun({ + owner, + repo, + run_id: run.id, + }); + } + } + catch (e) { + if (e.status === 403) { + core_1.default.warning(`Failed to delete workflow with 403 permission error: path: ${wf.path}, workflow_run_id: ${run.id}, message: ${e.message}. It's probably present in another branch. Skipping...`); + continue; + } + throw e; + } + totalDeleted++; + } + } + core_1.default.info(`Deleted ${totalDeleted} workflow runs`); + }); +} +exports.pruneGithubWorkflowRuns = pruneGithubWorkflowRuns; +// Run the function above. +pruneGithubWorkflowRuns(); +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/prune-github-workflow-runs/lib/main.js.map b/prune-github-workflow-runs/lib/main.js.map new file mode 100644 index 0000000..987ce18 --- /dev/null +++ b/prune-github-workflow-runs/lib/main.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yDAAiC;AACjC,6DAAqC;AACrC,yDAAiC;AAEjC,2CAA6B;AAE7B,MAAM,KAAK,GAAW,cAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AACjE,MAAM,KAAK,GAAW,cAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAClE,MAAM,IAAI,GAAW,cAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAChE,MAAM,MAAM,GAAY,cAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,KAAK,OAAO,CAAC,CAAC,qEAAqE;AAEnI,SAAsB,uBAAuB;;QAC3C,MAAM,QAAQ,GAAG,gBAAM,CAAC,UAAU,CAAC,KAAK,EAAE;YACxC,QAAQ,EAAE;gBACR,WAAW,EAAE,CAAC,UAAU,GAAG,CAAC,EAAE,OAAY,EAAE,EAAE;oBAC5C,OAAO,CAAC,IAAI,CACV,uCAAuC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CACvE,CAAC;oBACF,IAAI,OAAO,CAAC,OAAO,CAAC,UAAU,KAAK,CAAC,EAAE;wBACpC,oBAAoB;wBACpB,OAAO,CAAC,GAAG,CAAC,kBAAkB,UAAU,WAAW,CAAC,CAAC;wBACrD,OAAO,IAAI,CAAC;qBACb;gBACH,CAAC;gBACD,YAAY,EAAE,CAAC,UAAU,GAAG,CAAC,EAAE,OAAY,EAAE,EAAE;oBAC7C,OAAO,CAAC,IAAI,CACV,8BAA8B,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAC9D,CAAC;oBACF,IAAI,OAAO,CAAC,OAAO,CAAC,UAAU,KAAK,CAAC,EAAE;wBACpC,oBAAoB;wBACpB,OAAO,CAAC,GAAG,CAAC,kBAAkB,UAAU,WAAW,CAAC,CAAC;wBACrD,OAAO,IAAI,CAAC;qBACb;gBACH,CAAC;aACF;YACD,QAAQ,EAAE,CAAC,SAAS,CAAC;SACtB,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,CAAC,wBAAwB,EAAE,0BAA0B,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,MAAM,cAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD,MAAM,iBAAiB,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,0BAA0B,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CACpE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CACxB,CAAC;QAEF,IAAI,0BAA0B,CAAC,MAAM,KAAK,CAAC,EAAE;YAC3C,cAAI,CAAC,SAAS,CACZ,wFAAwF,CACzF,CAAC;YACF,OAAO;SACR;QAED,cAAI,CAAC,IAAI,CACP,sDAAsD,0BAA0B,CAAC,IAAI,CACnF,IAAI,CACL,EAAE,CACJ,CAAC;QAEF,MAAM,gBAAgB,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAC9C,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EACvC;YACE,KAAK;YACL,IAAI;SACL,EACD,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAC5B,CAAC;QAEF,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,MAAM,CAC/C,CAAC,QAAQ,EAAE,EAAE,CACX,CAAC,0BAA0B,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CACrE,CAAC;QAEF,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,cAAI,CAAC,IAAI,CACP;QACI,iBAAiB,CAAC,MAAM;EAC9B,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;oCACzC,CACjC,CAAC;QAEF,KAAK,MAAM,EAAE,IAAI,iBAAiB,EAAE;YAClC,cAAI,CAAC,IAAI,CAAC,sCAAsC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAE5D,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAC1C,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,gBAAgB,EACtC;gBACE,KAAK;gBACL,IAAI;gBACJ,WAAW,EAAE,EAAE,CAAC,EAAE;aACnB,EACD,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAC5B,CAAC;YAEF,KAAK,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,YAAY,CAAC,OAAO,EAAE,EAAE;gBACjD,cAAI,CAAC,IAAI,CACP,cAAc,EAAE,CAAC,IAAI,qBAAqB,KAAK,GAAG,CAAC,IACjD,YAAY,CAAC,MACf,eAAe,GAAG,CAAC,EAAE,EAAE,CACxB,CAAC;gBACF,IAAI;oBACF,IAAI,CAAC,MAAM,EAAE;wBACX,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC;4BAC5C,KAAK;4BACL,IAAI;4BACJ,MAAM,EAAE,GAAG,CAAC,EAAE;yBACf,CAAC,CAAC;qBACJ;iBACF;gBAAC,OAAO,CAAM,EAAE;oBACf,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG,EAAE;wBACpB,cAAI,CAAC,OAAO,CACV,8DAA8D,EAAE,CAAC,IAAI,sBAAsB,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC,OAAO,wDAAwD,CACjL,CAAC;wBACF,SAAS;qBACV;oBACD,MAAM,CAAC,CAAC;iBACT;gBACD,YAAY,EAAE,CAAC;aAChB;SACF;QAED,cAAI,CAAC,IAAI,CAAC,WAAW,YAAY,gBAAgB,CAAC,CAAC;IACrD,CAAC;CAAA;AA/GD,0DA+GC;AAED,0BAA0B;AAC1B,uBAAuB,EAAE,CAAC"} \ No newline at end of file diff --git a/prune-github-workflow-runs/package.json b/prune-github-workflow-runs/package.json new file mode 100644 index 0000000..d6e019a --- /dev/null +++ b/prune-github-workflow-runs/package.json @@ -0,0 +1,19 @@ +{ + "name": "prune-github-workflow-runs", + "version": "0.0.1", + "main": "lib/main.js", + "scripts": { + "build": "tsc" + }, + "license": "Apache-2.0", + "dependencies": { + "@actions/core": "^1.9.1", + "@actions/github": "^5.0.3", + "@actions/glob": "^0.3.0" + }, + "devDependencies": { + "@types/node": "^16.10.5", + "ts-node": "^10.8.0", + "typescript": "^4.4.4" + } +} \ No newline at end of file diff --git a/prune-github-workflow-runs/pnpm-lock.yaml b/prune-github-workflow-runs/pnpm-lock.yaml new file mode 100644 index 0000000..616748f --- /dev/null +++ b/prune-github-workflow-runs/pnpm-lock.yaml @@ -0,0 +1,359 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@actions/core': + specifier: ^1.9.1 + version: 1.10.1 + '@actions/github': + specifier: ^5.0.3 + version: 5.1.1 + '@actions/glob': + specifier: ^0.3.0 + version: 0.3.0 + +devDependencies: + '@types/node': + specifier: ^16.10.5 + version: 16.18.96 + ts-node: + specifier: ^10.8.0 + version: 10.9.2(@types/node@16.18.96)(typescript@4.9.5) + typescript: + specifier: ^4.4.4 + version: 4.9.5 + +packages: + + /@actions/core@1.10.1: + resolution: {integrity: sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==} + dependencies: + '@actions/http-client': 2.2.1 + uuid: 8.3.2 + dev: false + + /@actions/github@5.1.1: + resolution: {integrity: sha512-Nk59rMDoJaV+mHCOJPXuvB1zIbomlKS0dmSIqPGxd0enAXBnOfn4VWF+CGtRCwXZG9Epa54tZA7VIRlJDS8A6g==} + dependencies: + '@actions/http-client': 2.2.1 + '@octokit/core': 3.6.0 + '@octokit/plugin-paginate-rest': 2.21.3(@octokit/core@3.6.0) + '@octokit/plugin-rest-endpoint-methods': 5.16.2(@octokit/core@3.6.0) + transitivePeerDependencies: + - encoding + dev: false + + /@actions/glob@0.3.0: + resolution: {integrity: sha512-tJP1ZhF87fd6LBnaXWlahkyvdgvsLl7WnreW1EZaC8JWjpMXmzqWzQVe/IEYslrkT9ymibVrKyJN4UMD7uQM2w==} + dependencies: + '@actions/core': 1.10.1 + minimatch: 3.1.2 + dev: false + + /@actions/http-client@2.2.1: + resolution: {integrity: sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==} + dependencies: + tunnel: 0.0.6 + undici: 5.28.4 + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: false + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@octokit/auth-token@2.5.0: + resolution: {integrity: sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==} + dependencies: + '@octokit/types': 6.41.0 + dev: false + + /@octokit/core@3.6.0: + resolution: {integrity: sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==} + dependencies: + '@octokit/auth-token': 2.5.0 + '@octokit/graphql': 4.8.0 + '@octokit/request': 5.6.3 + '@octokit/request-error': 2.1.0 + '@octokit/types': 6.41.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/endpoint@6.0.12: + resolution: {integrity: sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==} + dependencies: + '@octokit/types': 6.41.0 + is-plain-object: 5.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/graphql@4.8.0: + resolution: {integrity: sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==} + dependencies: + '@octokit/request': 5.6.3 + '@octokit/types': 6.41.0 + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/openapi-types@12.11.0: + resolution: {integrity: sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==} + dev: false + + /@octokit/plugin-paginate-rest@2.21.3(@octokit/core@3.6.0): + resolution: {integrity: sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==} + peerDependencies: + '@octokit/core': '>=2' + dependencies: + '@octokit/core': 3.6.0 + '@octokit/types': 6.41.0 + dev: false + + /@octokit/plugin-rest-endpoint-methods@5.16.2(@octokit/core@3.6.0): + resolution: {integrity: sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==} + peerDependencies: + '@octokit/core': '>=3' + dependencies: + '@octokit/core': 3.6.0 + '@octokit/types': 6.41.0 + deprecation: 2.3.1 + dev: false + + /@octokit/request-error@2.1.0: + resolution: {integrity: sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==} + dependencies: + '@octokit/types': 6.41.0 + deprecation: 2.3.1 + once: 1.4.0 + dev: false + + /@octokit/request@5.6.3: + resolution: {integrity: sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==} + dependencies: + '@octokit/endpoint': 6.0.12 + '@octokit/request-error': 2.1.0 + '@octokit/types': 6.41.0 + is-plain-object: 5.0.0 + node-fetch: 2.7.0 + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /@octokit/types@6.41.0: + resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + dependencies: + '@octokit/openapi-types': 12.11.0 + dev: false + + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/node@16.18.96: + resolution: {integrity: sha512-84iSqGXoO+Ha16j8pRZ/L90vDMKX04QTYMTfYeE1WrjWaZXuchBehGUZEpNgx7JnmlrIHdnABmpjrQjhCnNldQ==} + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /ts-node@10.9.2(@types/node@16.18.96)(typescript@4.9.5): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 16.18.96 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: false + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + dev: true + + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: false + + /universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + dev: false + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true diff --git a/prune-github-workflow-runs/src/main.ts b/prune-github-workflow-runs/src/main.ts new file mode 100644 index 0000000..9b5338e --- /dev/null +++ b/prune-github-workflow-runs/src/main.ts @@ -0,0 +1,126 @@ +import core from "@actions/core"; +import github from "@actions/github"; +import glob from "@actions/glob"; + +import * as path from "path"; + +const token: string = core.getInput("token", { required: true }); +const owner: string = core.getInput("owner", { required: false }); +const repo: string = core.getInput("repo", { required: false }); +const dryRun: boolean = core.getInput("dry-run") !== "false"; // any value other than "false" will be considered as true to be safe + +export async function pruneGithubWorkflowRuns() { + const ghClient = github.getOctokit(token, { + throttle: { + onRateLimit: (retryAfter = 0, options: any) => { + console.warn( + `Request quota exhausted for request ${options.method} ${options.url}` + ); + if (options.request.retryCount === 0) { + // only retries once + console.log(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + onAbuseLimit: (retryAfter = 0, options: any) => { + console.warn( + `Abuse detected for request ${options.method} ${options.url}` + ); + if (options.request.retryCount === 0) { + // only retries once + console.log(`Retrying after ${retryAfter} seconds!`); + return true; + } + }, + }, + previews: ["ant-man"], + }); + + const patterns = [`.github/worklows/*.yml`, `.github/workflows/*.yaml`]; + const globber = await glob.create(patterns.join("\n")); + const workflowFilePaths = await globber.glob(); + const workflowFilesPresentInRepo = workflowFilePaths.map((filePath) => + path.basename(filePath) + ); + + if (workflowFilesPresentInRepo.length === 0) { + core.setFailed( + "Found 0 workflow files under `.github/workflows` which is kinda odd - exiting early..." + ); + return; + } + + core.info( + `\nFound the following workflow files in the repo:\n${workflowFilesPresentInRepo.join( + "\n" + )}` + ); + + const workflowResponse = await ghClient.paginate( + ghClient.rest.actions.listRepoWorkflows, + { + owner, + repo, + }, + (response) => response.data + ); + + const obsoleteWorkflows = workflowResponse.filter( + (workflow) => + !workflowFilesPresentInRepo.includes(path.basename(workflow.path)) + ); + + let totalDeleted = 0; + + core.info( + ` +Found ${obsoleteWorkflows.length} obsolete workflows: +${obsoleteWorkflows.map((wf) => `'${wf.name}' - path: ${wf.path}`).join("\n")} +Deleting their workflow runs now...` + ); + + for (const wf of obsoleteWorkflows) { + core.info("Deleting workflow runs of workflow: " + wf.name); + + const workflowRuns = await ghClient.paginate( + ghClient.rest.actions.listWorkflowRuns, + { + owner, + repo, + workflow_id: wf.id, + }, + (response) => response.data + ); + + for (const [index, run] of workflowRuns.entries()) { + core.info( + `Workflow: "${wf.name}" - Deleting Run (${index + 1}/${ + workflowRuns.length + }) - Run ID: ${run.id}` + ); + try { + if (!dryRun) { + await ghClient.rest.actions.deleteWorkflowRun({ + owner, + repo, + run_id: run.id, + }); + } + } catch (e: any) { + if (e.status === 403) { + core.warning( + `Failed to delete workflow with 403 permission error: path: ${wf.path}, workflow_run_id: ${run.id}, message: ${e.message}. It's probably present in another branch. Skipping...` + ); + continue; + } + throw e; + } + totalDeleted++; + } + } + + core.info(`Deleted ${totalDeleted} workflow runs`); +} + +// Run the function above. +pruneGithubWorkflowRuns(); diff --git a/prune-github-workflow-runs/tsconfig.json b/prune-github-workflow-runs/tsconfig.json new file mode 100644 index 0000000..a276dfb --- /dev/null +++ b/prune-github-workflow-runs/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "module": "commonjs", + "strict": true, + "declaration": false, + "target": "es6", + "sourceMap": true, + "lib": ["es6"], + "outDir": "lib", + "rootDir": "src", + "moduleResolution": "node", + }, + "exclude": ["node_modules", "__tests__/*.test.ts"], +} \ No newline at end of file