├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── __mocks__ └── @actions │ ├── core.js │ └── github.js ├── __tests__ ├── main.test.ts └── utils.test.ts ├── action.yml ├── dist └── index.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── main.ts └── utils.ts ├── tsconfig.json ├── types.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 100 10 | tab_width = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | 17 | [*.{yml,yaml}] 18 | max_line_length = off 19 | 20 | [COMMIT_EDITMSG] 21 | max_line_length = off 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/es6"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "no-unused-vars": "off", 14 | "@typescript-eslint/no-unused-vars": "error", 15 | "@typescript-eslint/explicit-member-accessibility": [ 16 | "error", 17 | { "accessibility": "no-public" } 18 | ], 19 | "@typescript-eslint/no-require-imports": "error", 20 | "@typescript-eslint/array-type": "error", 21 | "@typescript-eslint/await-thenable": "error", 22 | "@typescript-eslint/ban-ts-ignore": "error", 23 | "camelcase": "off", 24 | "@typescript-eslint/camelcase": "error", 25 | "@typescript-eslint/class-name-casing": "error", 26 | "@typescript-eslint/explicit-function-return-type": [ 27 | "error", 28 | { "allowExpressions": true } 29 | ], 30 | "@typescript-eslint/func-call-spacing": ["error", "never"], 31 | "@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"], 32 | "@typescript-eslint/no-array-constructor": "error", 33 | "@typescript-eslint/no-empty-interface": "error", 34 | "@typescript-eslint/no-explicit-any": "error", 35 | "@typescript-eslint/no-extraneous-class": "error", 36 | "@typescript-eslint/no-for-in-array": "error", 37 | "@typescript-eslint/no-inferrable-types": "error", 38 | "@typescript-eslint/no-misused-new": "error", 39 | "@typescript-eslint/no-namespace": "error", 40 | "@typescript-eslint/no-non-null-assertion": "warn", 41 | "@typescript-eslint/no-object-literal-type-assertion": "error", 42 | "@typescript-eslint/no-unnecessary-qualifier": "error", 43 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 44 | "@typescript-eslint/no-useless-constructor": "error", 45 | "@typescript-eslint/no-var-requires": "error", 46 | "@typescript-eslint/prefer-for-of": "warn", 47 | "@typescript-eslint/prefer-function-type": "warn", 48 | "@typescript-eslint/prefer-includes": "error", 49 | "@typescript-eslint/prefer-interface": "error", 50 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 51 | "@typescript-eslint/promise-function-async": "error", 52 | "@typescript-eslint/require-array-sort-compare": "error", 53 | "@typescript-eslint/restrict-plus-operands": "error", 54 | "semi": "off", 55 | "@typescript-eslint/semi": ["error", "never"], 56 | "@typescript-eslint/type-annotation-spacing": "error", 57 | "@typescript-eslint/unbound-method": "error" 58 | }, 59 | "env": { 60 | "node": true, 61 | "es6": true, 62 | "jest/globals": true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'build-test' 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | - 'releases/*' 8 | 9 | jobs: 10 | artifact: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Generate artifact to delete 14 | run: | 15 | echo $(date) > date.txt 16 | - name: Upload artifact 17 | uses: actions/upload-artifact@v1 18 | with: 19 | name: artifact 20 | path: date.txt 21 | build: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v1 25 | - run: | 26 | yarn 27 | yarn run all 28 | test: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v1 32 | - uses: ./ 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | expire-in: '30minutes' 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | 93 | # OS metadata 94 | .DS_Store 95 | Thumbs.db 96 | 97 | # IDE metadata 98 | .idea/ 99 | .vs/ 100 | .vscode/ 101 | 102 | # Ignore built ts files 103 | __tests__/runner/* 104 | lib/**/* -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.16.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 GitHub, Inc. and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # No longer maintained 2 | 3 | This project was created as temporary solution to problem I saw a lot of people to have but it seems people at github have no intention of fixing and neither do I. 4 | I recommend looking at other projects or you could use this as starting point for your own action. 5 | 6 | You could also +1 [this issue](https://github.com/actions/upload-artifact/issues/290) 7 | 8 | or take a look at this comment from the same issue https://github.com/actions/upload-artifact/issues/290#issuecomment-1374207010 9 | 10 | # Delete artifacts action 11 | 12 | Action responsible for deleting old artifacts by setting expire duration. 13 | 14 | Hopefuly this is just temporary solution till github implements this functionality natively. 15 | 16 | ## Inputs 17 | ### `expire-in` 18 | **Required** for how long the artifacts should be kept. 19 | Most of the human readable formats are supported `10 minutes`, `1hr 20mins`, `1week`. 20 | Take a look at [parse-duration](https://github.com/jkroso/parse-duration) for more information. 21 | 22 | 23 | ## Outputs 24 | ### `deleted-artifacts` 25 | Serialized list of deleted artifacts. Empty `[]` when nothing is deleted 26 | 27 | ## Usage 28 | 29 | Run this action as cron. This won't delete artifacts of running workflows because they 30 | are persisted after workflow completion. 31 | 32 | ```yaml 33 | name: 'Delete old artifacts' 34 | on: 35 | schedule: 36 | - cron: '0 * * * *' # every hour 37 | 38 | jobs: 39 | delete-artifacts: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: kolpav/purge-artifacts-action@v1 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | expire-in: 7days # Setting this to 0 will delete all artifacts 46 | ``` 47 | 48 | ## Optional arguments 49 | 50 | ### `onlyPrefix` 51 | 52 | Only purge artifacts that start with `tmp_` as a prefix. 53 | 54 | ```yaml 55 | with: 56 | onlyPrefix: tmp_ 57 | ``` 58 | 59 | ### `exceptPrefix` 60 | 61 | Exclude any artifacts that start with `prod_` as a prefix 62 | 63 | ```yaml 64 | with: 65 | exceptPrefix: prod_ 66 | ``` 67 | 68 | ## Note 69 | 70 | If you reach size limit, you can temporarily change to `on: push` and run it immediately. 71 | Even if the action succeeded, it will take a few more minutes for the artifacts to actually disappear. 72 | 73 | ## Contributing 74 | 75 | I would take a look at other maintained projects and contribute to them. 76 | -------------------------------------------------------------------------------- /__mocks__/@actions/core.js: -------------------------------------------------------------------------------- 1 | const core = jest.genMockFromModule("@actions/core") 2 | 3 | core.debug = console.log 4 | 5 | module.exports = core 6 | -------------------------------------------------------------------------------- /__mocks__/@actions/github.js: -------------------------------------------------------------------------------- 1 | const github = jest.genMockFromModule('@actions/github') 2 | 3 | github.context.repo = { 4 | repo: "https://github.com/kolpav/purge-artifacts-action", 5 | owner: "kolpav" 6 | } 7 | 8 | module.exports = github 9 | -------------------------------------------------------------------------------- /__tests__/main.test.ts: -------------------------------------------------------------------------------- 1 | import { main, shouldDelete } from '../src/main' 2 | import { sub } from 'date-fns' 3 | import { IActionInputs } from '../src/utils' 4 | 5 | describe('shouldDelete', () => { 6 | test('expired', () => { 7 | const days = 2 8 | const expireInMs = days * 86400000 9 | const expiredArtifact = { created_at: sub(new Date(), { days }) } 10 | const actionInptus: IActionInputs = { 11 | expireInMs, 12 | onlyPrefix: '', 13 | exceptPrefix: '' 14 | } 15 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(true) 16 | }) 17 | test('not expired', () => { 18 | const days = 2 19 | const expireInMs = (days + 1) * 86400000 20 | const expiredArtifact = { created_at: sub(new Date(), { days }) } 21 | const actionInptus: IActionInputs = { 22 | expireInMs, 23 | onlyPrefix: '', 24 | exceptPrefix: '' 25 | } 26 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(false) 27 | }) 28 | test('expired when expireInDays is zero', () => { 29 | const expiredArtifact = { created_at: new Date() } 30 | const actionInptus: IActionInputs = { 31 | expireInMs: 0, 32 | onlyPrefix: '', 33 | exceptPrefix: '' 34 | } 35 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(true) 36 | }) 37 | test('should delete when matched by onlyPrefix', () => { 38 | const expiredArtifact = { 39 | created_at: new Date(), 40 | name: 'tmp_artifact.test' 41 | } 42 | const actionInptus: IActionInputs = { 43 | expireInMs: 0, 44 | onlyPrefix: 'tmp', 45 | exceptPrefix: '' 46 | } 47 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(true) 48 | }) 49 | test('should not delete when not matched by onlyPrefix', () => { 50 | const expiredArtifact = { 51 | created_at: new Date(), 52 | name: 'build_artifact.test' 53 | } 54 | const actionInptus: IActionInputs = { 55 | expireInMs: 0, 56 | onlyPrefix: 'tmp', 57 | exceptPrefix: '' 58 | } 59 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(false) 60 | }) 61 | test('should delete when not matched by exceptPrefix', () => { 62 | const expiredArtifact = { 63 | created_at: new Date(), 64 | name: 'tmp_artifact.test' 65 | } 66 | const actionInptus: IActionInputs = { 67 | expireInMs: 0, 68 | onlyPrefix: '', 69 | exceptPrefix: 'master_' 70 | } 71 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(true) 72 | }) 73 | test('should not delete when matched by exceptPrefix', () => { 74 | const expiredArtifact = { 75 | created_at: new Date(), 76 | name: 'master_artifact.test' 77 | } 78 | const actionInptus: IActionInputs = { 79 | expireInMs: 0, 80 | onlyPrefix: '', 81 | exceptPrefix: 'master_' 82 | } 83 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(false) 84 | }) 85 | test('should not delete when matched by both onlyPrefix and exceptPrefix', () => { 86 | const expiredArtifact = { 87 | created_at: new Date(), 88 | name: 'master_tmp_artifact.test' 89 | } 90 | const actionInptus: IActionInputs = { 91 | expireInMs: 0, 92 | onlyPrefix: 'master_', 93 | exceptPrefix: 'master_tmp_' 94 | } 95 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(false) 96 | }) 97 | test('should delete when matched by onlyPrefix but not exceptPrefix', () => { 98 | const expiredArtifact = { 99 | created_at: new Date(), 100 | name: 'master_tmp_artifact.test' 101 | } 102 | const actionInptus: IActionInputs = { 103 | expireInMs: 0, 104 | onlyPrefix: 'master_', 105 | exceptPrefix: 'tmp_' 106 | } 107 | expect(shouldDelete(expiredArtifact as any, actionInptus)).toEqual(true) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { eachArtifact } from '../src/utils' 2 | 3 | describe('eachArtifact', () => { 4 | test('called with correct arguments', async () => { 5 | const octokit = { 6 | actions: { 7 | listArtifactsForRepo: jest.fn(async () => ({ 8 | data: { 9 | artifacts: [], 10 | total_count: 0 11 | } 12 | })) 13 | } 14 | } 15 | for await (const artifact of eachArtifact(octokit as any)) { 16 | } 17 | expect(octokit.actions.listArtifactsForRepo).toBeCalledWith({ 18 | owner: 'kolpav', 19 | repo: 'https://github.com/kolpav/purge-artifacts-action', 20 | page: 1, 21 | // eslint-disable-line @typescript-eslint/camelcase 22 | per_page: 100 23 | }) 24 | }) 25 | test('iterates over all artifacts', async () => { 26 | const maxPerPage = 100 27 | const totalCount = 117 28 | const artifacts = [] 29 | for (let i = 0; i < totalCount; i++) { 30 | artifacts[i] = i 31 | } 32 | const firstListArtifactsForRepoResponse = { 33 | data: { 34 | artifacts: artifacts.slice(0, maxPerPage), 35 | // eslint-disable-line @typescript-eslint/camelcase 36 | total_count: totalCount 37 | } 38 | } 39 | const secondListArtifactsForRepoResponse = { 40 | data: { 41 | artifacts: artifacts.slice(maxPerPage, artifacts.length), 42 | // eslint-disable-line @typescript-eslint/camelcase 43 | total_count: totalCount 44 | } 45 | } 46 | const listArtifactsForRepoMock = jest 47 | .fn() 48 | .mockResolvedValueOnce(firstListArtifactsForRepoResponse) 49 | .mockResolvedValueOnce(secondListArtifactsForRepoResponse) 50 | const octokit = { 51 | actions: { 52 | listArtifactsForRepo: listArtifactsForRepoMock 53 | } 54 | } 55 | let artifactIndex = 0 56 | for await (const artifact of eachArtifact(octokit as any)) { 57 | expect(artifact).toEqual(artifacts[artifactIndex++]) 58 | } 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Purge artifacts' 2 | description: 'Automatically delete artifacts' 3 | branding: 4 | icon: 'trash-2' 5 | color: 'red' 6 | author: 'Pavel Kolář' 7 | inputs: 8 | expire-in: 9 | required: true 10 | description: 'Human readable expire duration' 11 | token: 12 | required: true 13 | description: 'Github auth token' 14 | onlyPrefix: 15 | required: false 16 | description: 'Only artifacts with the specified prefix will be deleted' 17 | default: '' 18 | exceptPrefix: 19 | required: false 20 | description: 'Artifacts with this prefix will not be included for deletion' 21 | default: '' 22 | runs: 23 | using: 'node12' 24 | main: 'dist/index.js' 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purge-artifacts-action", 3 | "version": "0.0.1", 4 | "description": "Github action for automatic deletion of artifacts", 5 | "main": "lib/main.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "format": "prettier --write **/*.{ts,yml,json}", 9 | "format-check": "prettier --check **/*.{ts,yml,json}", 10 | "lint": "eslint src/**/*.ts", 11 | "pack": "ncc build", 12 | "test": "jest", 13 | "test-watch": "jest --watch", 14 | "all": "yarn run build && yarn run format && yarn run lint && yarn run pack && yarn run test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/kolpav/purge-artifacts-action" 19 | }, 20 | "keywords": [ 21 | "github", 22 | "action", 23 | "artifact" 24 | ], 25 | "author": "Pavel Kolář", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@actions/core": "^1.2.0", 29 | "@actions/github": "^2.1.1", 30 | "@octokit/core": "^2.4.2", 31 | "@octokit/plugin-rest-endpoint-methods": "^3.2.0", 32 | "date-fns": "^2.9.0", 33 | "parse-duration": "^0.1.2" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^24.0.23", 37 | "@types/node": "^12.7.12", 38 | "@typescript-eslint/parser": "^2.8.0", 39 | "@zeit/ncc": "^0.20.5", 40 | "eslint": "^5.16.0", 41 | "eslint-plugin-github": "^2.0.0", 42 | "eslint-plugin-jest": "^22.21.0", 43 | "jest": "^24.9.0", 44 | "jest-circus": "^24.9.0", 45 | "js-yaml": "^3.13.1", 46 | "prettier": "^1.19.1", 47 | "ts-jest": "^24.2.0", 48 | "typescript": "^3.6.4" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core' 2 | import * as github from '@actions/github' 3 | import differenceInMilliseconds from 'date-fns/differenceInMilliseconds' 4 | import { 5 | getOctokit, 6 | eachArtifact, 7 | getActionInputs, 8 | IActionInputs 9 | } from './utils' 10 | 11 | export function shouldDelete( 12 | artifact: ActionsListArtifactsForRepoResponseArtifactsItem, 13 | actionInputs: IActionInputs 14 | ): boolean { 15 | const { expireInMs, onlyPrefix, exceptPrefix } = actionInputs 16 | 17 | const included = onlyPrefix === '' || artifact.name.startsWith(onlyPrefix) 18 | const excluded = exceptPrefix && artifact.name.startsWith(exceptPrefix) 19 | const expired = 20 | differenceInMilliseconds(new Date(), new Date(artifact.created_at)) >= 21 | expireInMs 22 | 23 | return included && !excluded && expired 24 | } 25 | 26 | export async function main(): Promise { 27 | try { 28 | const actionInputs = getActionInputs() 29 | 30 | const octokit = getOctokit() 31 | 32 | const deletedArtifacts = [] 33 | for await (const artifact of eachArtifact(octokit)) { 34 | if (shouldDelete(artifact, actionInputs)) { 35 | deletedArtifacts.push(artifact) 36 | core.debug(`Deleting artifact:\n${JSON.stringify(artifact, null, 2)}`) 37 | await octokit.actions.deleteArtifact({ 38 | owner: github.context.repo.owner, 39 | repo: github.context.repo.repo, 40 | // eslint-disable-next-line @typescript-eslint/camelcase 41 | artifact_id: artifact.id 42 | }) 43 | } 44 | } 45 | core.setOutput('deleted-artifacts', JSON.stringify(deletedArtifacts)) 46 | } catch (error) { 47 | core.setFailed(error.message) 48 | } 49 | } 50 | 51 | main() 52 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from '@octokit/core' 2 | import * as core from '@actions/core' 3 | import * as github from '@actions/github' 4 | import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods' 5 | import { RestEndpointMethods } from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/types' 6 | import parseDuration from 'parse-duration' 7 | 8 | /* 9 | We need to create our own github client because @actions/core still uses 10 | old version of @octokit/plugin-rest-endpoint-methods which doesn't have 11 | `.listArtifactsForRepo`. This won't be needed when @actions/core gets updated 12 | 13 | This ---------------> https://github.com/actions/toolkit/blob/master/packages/github/package.json#L42 14 | https://github.com/octokit/rest.js/blob/master/package.json#L38 15 | Neeeds to use this -> https://github.com/octokit/plugin-rest-endpoint-methods.js/pull/45 16 | */ 17 | export function getOctokit(): Octokit & RestEndpointMethods { 18 | const token = core.getInput('token', { required: true }) 19 | const _Octokit = Octokit.plugin(restEndpointMethods) 20 | return new _Octokit({ auth: token }) 21 | // return new github.GitHub(token) 22 | } 23 | 24 | export async function* eachArtifact( 25 | octokit: Octokit & RestEndpointMethods 26 | ): AsyncGenerator { 27 | let hasNextPage = false 28 | let currentPage = 1 29 | const maxPerPage = 100 30 | do { 31 | const response = await octokit.actions.listArtifactsForRepo({ 32 | owner: github.context.repo.owner, 33 | repo: github.context.repo.repo, 34 | page: currentPage, 35 | per_page: maxPerPage // eslint-disable-line @typescript-eslint/camelcase 36 | }) 37 | hasNextPage = response.data.total_count / maxPerPage > currentPage 38 | for (const artifact of response.data.artifacts) { 39 | yield artifact 40 | } 41 | currentPage++ 42 | } while (hasNextPage) 43 | } 44 | 45 | export interface IActionInputs { 46 | expireInMs: number 47 | onlyPrefix: string 48 | exceptPrefix: string 49 | } 50 | export function getActionInputs(): IActionInputs { 51 | const expireInHumanReadable = core.getInput('expire-in', { required: true }) 52 | const expireInMs = parseDuration(expireInHumanReadable) 53 | const onlyPrefix = core.getInput('onlyPrefix', { required: false }) 54 | const exceptPrefix = core.getInput('exceptPrefix', { required: false }) 55 | 56 | return { expireInMs, onlyPrefix, exceptPrefix } 57 | } 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 4 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 5 | "outDir": "./lib" /* Redirect output structure to the directory. */, 6 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 9 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 10 | }, 11 | "exclude": ["node_modules", "**/*.test.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare type ActionsListArtifactsForRepoResponseArtifactsItem = { 2 | archive_download_url: string 3 | created_at: string 4 | expired: string 5 | expires_at: string 6 | id: number 7 | name: string 8 | node_id: string 9 | size_in_bytes: number 10 | url: string 11 | } 12 | 13 | declare module 'parse-duration' { 14 | export = parse 15 | 16 | /** 17 | * convert `str` to ms 18 | */ 19 | function parse (str: string): number 20 | } 21 | --------------------------------------------------------------------------------