├── .gitignore ├── .editorconfig ├── src ├── env.d.ts ├── artifact.ts ├── utils.ts ├── index.ts ├── artifact-filter.ts └── artifact-client.ts ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── example.yml ├── action.yml ├── CHANGELOG.md ├── package.json ├── tsconfig.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | end_of_line = crlf 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | /** 5 | * Current workflow run identifier. 6 | */ 7 | GITHUB_RUN_ID: string; 8 | 9 | /** 10 | * Repository of this workflow run in the format {OWNER}/{REPOSITORY}. 11 | */ 12 | GITHUB_REPOSITORY: string; 13 | } 14 | } 15 | } 16 | 17 | export {}; 18 | 19 | -------------------------------------------------------------------------------- /src/artifact.ts: -------------------------------------------------------------------------------- 1 | import type * as github from "@actions/github"; 2 | 3 | /** 4 | * Collection of artifacts associated with a workflow run. 5 | */ 6 | export type ListWorkflowRunArtifactsResponse = ReturnType< 7 | ReturnType< 8 | typeof github.getOctokit 9 | >["rest"]["actions"]["listWorkflowRunArtifacts"] 10 | > extends Promise 11 | ? X 12 | : never; 13 | 14 | /** 15 | * Workflow run artifact. 16 | */ 17 | export type Artifact = 18 | ListWorkflowRunArtifactsResponse["data"]["artifacts"] extends (infer ElementType)[] 19 | ? ElementType 20 | : never; 21 | 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [GeekyEggo] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Delete Artifact 2 | description: Delete artifacts created within the workflow run. 3 | inputs: 4 | name: 5 | description: The name of the artifact to delete; multiple names can be supplied on new lines. 6 | required: true 7 | token: 8 | description: GitHub token with read and write access to actions for the repository. 9 | required: true 10 | default: ${{ github.token }} 11 | useGlob: 12 | description: Indicates whether the name, or names, should be treated as glob patterns. 13 | required: false 14 | default: "true" 15 | failOnError: 16 | description: Indicates whether the action should fail upon encountering an error. 17 | required: false 18 | default: "true" 19 | runs: 20 | using: node20 21 | main: ./dist/index.js 22 | branding: 23 | icon: trash-2 24 | color: red 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | # Change Log 13 | 14 | ## v4.1 15 | 16 | - Add default token. 17 | - Fix over-arching `catch` output; errors now correctly result in a failed run ([@TheMrMilchmann ](https://github.com/TheMrMilchmann)). 18 | 19 | ## v4.0 20 | 21 | - Add support for artifacts uploaded with `actions/upload-artifact@v4`. 22 | - Add requirement of `token` with read and write access to actions. 23 | - Update requests to use GitHub REST API. 24 | - Deprecate support for `actions/upload-artifact@v1`, `actions/upload-artifact@v2`, and `actions/upload-artifact@v3` (please use `geekyeggo/delete-artifact@v2`). 25 | 26 | ## v2.0 27 | 28 | - Add support for glob pattern matching via `useGlob`. 29 | 30 | ## v1.0 31 | 32 | - Initial release. 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delete-artifact", 3 | "description": "Deletes artifacts from a workflow run", 4 | "version": "4.1.0", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "build": "ncc build", 8 | "watch": "ncc build -w" 9 | }, 10 | "keywords": [ 11 | "github", 12 | "github-actions", 13 | "delete-artifact", 14 | "artifact" 15 | ], 16 | "author": "GeekyEggo", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/geekyeggo/delete-artifact" 21 | }, 22 | "dependencies": { 23 | "@actions/core": "^1.10.0", 24 | "@actions/github": "^6.0.0", 25 | "minimatch": "^9.0.3" 26 | }, 27 | "devDependencies": { 28 | "@types/minimatch": "^5.1.2", 29 | "@types/node": "^20.10.5", 30 | "@vercel/ncc": "^0.38.1", 31 | "typescript": "^5.3.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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": [ 12 | "node_modules" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Richard Herman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | 3 | /** 4 | * Attempts to fail the action based on the `failOnError` input; otherwise an error is logged. 5 | * @param msg The message to log as the error, or the failure. 6 | */ 7 | export function fail(msg: string): void { 8 | if (getInputBoolean("failOnError")) { 9 | core.setFailed(msg); 10 | } else { 11 | core.error(msg); 12 | } 13 | } 14 | 15 | /** 16 | * Attempts to get a truthy input based on the specified name. 17 | * @param name The name of the input property. 18 | * @returns `true` when the input property is truthy; otherwise false. 19 | */ 20 | export function getInputBoolean(name: string): boolean { 21 | const value = core.getInput(name); 22 | return value === "true" || value === "1"; 23 | } 24 | 25 | /** 26 | * Gets the input for the specified name, and splits the value by new line. 27 | * Credit to https://github.com/softprops 28 | * @param name The name of the input property. 29 | * @returns The values. 30 | */ 31 | export function getInputMultilineValues(name: string): string[] { 32 | return core 33 | .getInput(name) 34 | .split(/\r?\n/) 35 | .filter((name) => name) 36 | .map((name) => name.trim()); 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { ArtifactClient } from "./artifact-client"; 3 | import { getDefaultFilter } from "./artifact-filter"; 4 | import { fail } from "./utils"; 5 | 6 | (async function () { 7 | try { 8 | const client = new ArtifactClient(core.getInput("token")); 9 | let failureCount = 0; 10 | 11 | // Get the artifacts associated with this workflow run. 12 | const artifacts = await client.list(); 13 | const filter = getDefaultFilter(); 14 | 15 | // Iterate over the filtered artifacts, and remove them. 16 | for (const { id, name } of filter(artifacts)) { 17 | if (await client.del(id)) { 18 | core.info(`Successfully deleted artifact: "${name}"`); 19 | } else { 20 | core.error(`Failed to delete artifact: "${name}"`); 21 | failureCount++; 22 | } 23 | } 24 | 25 | if (failureCount > 0) { 26 | fail( 27 | `Failed to delete ${failureCount} artifact${ 28 | failureCount !== 1 ? "s" : "" 29 | }.` 30 | ); 31 | } 32 | } catch (err) { 33 | core.setFailed(err); 34 | } 35 | })(); 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Create test file 21 | run: echo hello > world.txt 22 | 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: my-artifact 26 | path: world.txt 27 | 28 | - uses: actions/upload-artifact@v4 29 | with: 30 | name: my-artifact-2 31 | path: world.txt 32 | 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: my-artifact-3 36 | path: world.txt 37 | 38 | - uses: actions/upload-artifact@v4 39 | with: 40 | name: you-artifact 41 | path: world.txt 42 | 43 | - name: Delete (specific, glob disabled) 44 | uses: ./ 45 | with: 46 | name: my-artifact 47 | useGlob: false 48 | 49 | - name: Delete (pattern, glob enabled) 50 | uses: ./ 51 | with: 52 | name: my-* 53 | 54 | - name: Delete (specific, glob enabled, and token specified) 55 | uses: ./ 56 | with: 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | name: you-artifact 59 | -------------------------------------------------------------------------------- /.github/workflows/example.yml: -------------------------------------------------------------------------------- 1 | name: Example 2 | # an example workflow that also monitors success of the preview api 3 | 4 | on: 5 | schedule: 6 | - cron: "0 */6 * * *" 7 | # every 6 hours 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Create test file 22 | run: echo hello > world.txt 23 | 24 | - uses: actions/upload-artifact@v4 25 | with: 26 | name: my-artifact 27 | path: world.txt 28 | 29 | - uses: actions/upload-artifact@v4 30 | with: 31 | name: my-artifact-2 32 | path: world.txt 33 | 34 | - uses: actions/upload-artifact@v4 35 | with: 36 | name: my-artifact-3 37 | path: world.txt 38 | 39 | - uses: actions/upload-artifact@v4 40 | with: 41 | name: you-artifact 42 | path: world.txt 43 | 44 | - name: Delete (specific, glob disabled) 45 | uses: geekyeggo/delete-artifact@v4 46 | with: 47 | name: my-artifact 48 | useGlob: false 49 | 50 | - name: Delete (pattern, glob enabled) 51 | uses: geekyeggo/delete-artifact@v4 52 | with: 53 | name: my-* 54 | 55 | - name: Delete (specific, glob enabled, with token) 56 | uses: geekyeggo/delete-artifact@v4 57 | with: 58 | name: you-artifact 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /src/artifact-filter.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core"; 2 | import { minimatch } from "minimatch"; 3 | import { Artifact } from "./artifact"; 4 | import { getInputBoolean, getInputMultilineValues } from "./utils"; 5 | 6 | /** 7 | * Filters artifacts based on the actions configuration. 8 | */ 9 | type Filter = (artifacts: Artifact[]) => IterableIterator; 10 | 11 | /** 12 | * Gets a filter that enables filtering of artifacts based on an exact match of their name. 13 | * @param names The names to match. 14 | * @returns The exact match filter. 15 | */ 16 | function getExactMatchFilter(names: string[]): Filter { 17 | return function* filter(artifacts: Artifact[]): IterableIterator { 18 | for (const name of names) { 19 | const artifact = artifacts.find((a) => a.name == name); 20 | if (artifact != null) { 21 | yield artifact; 22 | } else { 23 | core.warning( 24 | `Unable to delete artifact "${name}"; the artifact was not found.` 25 | ); 26 | } 27 | } 28 | }; 29 | } 30 | 31 | /** 32 | * Gets a filter that enables filtering of artifacts based on glob-pattern matching their name. 33 | * @param names The names to match. 34 | * @returns The glob match filter. 35 | */ 36 | function getGlobMatchFilter(names: string[]): Filter { 37 | const isMatch = (artifact: Artifact): boolean => 38 | names.some((pattern) => minimatch(artifact.name, pattern)); 39 | 40 | return function* filter(artifacts: Artifact[]): IterableIterator { 41 | for (const artifact of artifacts.filter(isMatch)) { 42 | yield artifact; 43 | } 44 | }; 45 | } 46 | 47 | /** 48 | * Gets the default filter based on the action settings. 49 | * @returns The filter to be used when determining which artifacts to delete. 50 | */ 51 | export function getDefaultFilter(): Filter { 52 | const names = getInputMultilineValues("name"); 53 | 54 | return getInputBoolean("useGlob") 55 | ? getGlobMatchFilter(names) 56 | : getExactMatchFilter(names); 57 | } 58 | -------------------------------------------------------------------------------- /src/artifact-client.ts: -------------------------------------------------------------------------------- 1 | import * as github from "@actions/github"; 2 | import { Artifact } from "./artifact"; 3 | 4 | /** 5 | * Client responsible for interacting with workflow run artifacts. 6 | */ 7 | export class ArtifactClient { 8 | /** 9 | * Octokit client responsible for making calls to GitHub. 10 | */ 11 | private readonly octokit: ReturnType; 12 | 13 | /** 14 | * Initializes a new instance of the {@link ArtifactClient}. 15 | * @param token GitHub token with read and write access to actions. 16 | */ 17 | constructor(token: string) { 18 | this.octokit = github.getOctokit(token); 19 | } 20 | 21 | /** 22 | * Deletes the specified artifact. 23 | * @param artifactId Artifact identifier. 24 | * @returns `true` when deletion was successful; otherwise `false`. 25 | */ 26 | public async del(artifactId: number): Promise { 27 | const { status } = await this.octokit.rest.actions.deleteArtifact({ 28 | ...github.context.repo, 29 | artifact_id: artifactId, 30 | }); 31 | 32 | return this.success(status); 33 | } 34 | 35 | /** 36 | * Lists the artifacts associated with the workflow run. 37 | * @returns The artifacts. 38 | */ 39 | public async list(): Promise { 40 | const res = await this.octokit.rest.actions.listWorkflowRunArtifacts({ 41 | ...github.context.repo, 42 | run_id: parseInt(process.env.GITHUB_RUN_ID), 43 | }); 44 | 45 | if (!this.success(res.status)) { 46 | throw new Error("Failed to load artifacts"); 47 | } 48 | 49 | return res.data.artifacts; 50 | } 51 | 52 | /** 53 | * Determines whether the specified {@link statusCode} denotes a successful response. 54 | * @param statusCode Status code. 55 | * @returns `true` when the status code is 2xx. 56 | */ 57 | private success(statusCode: number): boolean { 58 | return ( 59 | statusCode !== undefined && statusCode >= 200 && statusCode < 300 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/GeekyEggo/delete-artifact/workflows/CI/badge.svg) 2 | ![Example](https://github.com/GeekyEggo/delete-artifact/workflows/Example/badge.svg) 3 | 4 | # Delete artifacts 5 | 6 | A GitHub Action for deleting artifacts within the workflow run. This can be useful when artifacts are shared across jobs, but are no longer needed when the workflow is complete. 7 | 8 | ## ✅ Compatibility 9 | 10 | | `actions/upload-artifact` | `geekyeggo/delete-artifact` | 11 | | ------------------------- | --------------------------- | 12 | | `@v1`, `@v2`, `@v3` | `@v1`, `@v2` | 13 | | `@v4` | `@v4` | 14 | 15 | ## ⚡ Usage 16 | 17 | See [action.yml](action.yml) 18 | 19 | > [!IMPORTANT] 20 | > Support for `actions/upload-artifact@v4` utilizes the GitHub REST API, and requires a permissive [`GITHUB_TOKEN`](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token), or a PAT with read and write access to `actions`. 21 | 22 | ### Delete an individual artifact 23 | 24 | ```yml 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Create test file 30 | run: echo hello > test.txt 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: my-artifact 35 | path: test.txt 36 | 37 | - uses: geekyeggo/delete-artifact@v4 38 | with: 39 | name: my-artifact 40 | ``` 41 | 42 | ### Specify multiple names 43 | 44 | ```yml 45 | steps: 46 | - uses: geekyeggo/delete-artifact@v4 47 | with: 48 | name: | 49 | artifact-* 50 | binary-file 51 | output 52 | ``` 53 | 54 | ## 🚨 Error vs Fail 55 | 56 | By default, the action will fail when it was not possible to delete an artifact (with the exception of name mismatches). When the deletion of an artifact is not integral to the success of a workflow, it is possible to error without failure. All errors are logged. 57 | 58 | ```yml 59 | steps: 60 | - uses: geekyeggo/delete-artifact@v4 61 | with: 62 | name: okay-to-keep 63 | failOnError: false 64 | ``` 65 | --------------------------------------------------------------------------------