Issue #1005
GitLab CI/CD pipelines create artifacts that take up a lot of storage space. While GitLab has a UI to delete them, doing this for thousands of artifacts one page at a time is slow and boring.
In this article, we’ll build a script that uses GitLab’s GraphQL API to delete artifacts automatically.
Why Use GraphQL API?
The GraphQL API allow us to:
- Get data with pagination
- Delete up to 20 artifacts in one request
- Get exactly what you need
When you delete artifacts in GitLab’s web page, it uses the same GraphQL API we’ll use. Open your browser’s Network tab and click “Delete” - you’ll see it calling bulkDestroyJobArtifacts.
What You Need
- Access Token: Create one in GitLab Settings → Access Tokens with
apiscope - Project ID: Found in Project Settings → General (e.g.,
558) - Project Path: Your repo path (e.g.,
your-org/your-repo) - GitLab URL: Your GitLab URL (e.g.,
https://gitlab.com)
Let’s build this step by step.
Step 1: Setup
First, set up your config:
async function deleteAllArtifacts() {
// Update these values
const projectId = `gid://gitlab/Project/558`;
const projectPath = `your-org/your-repo`;
const accessToken = `glpat-YOUR_TOKEN_HERE`;
const baseUrl = `https://gitlab.com`;
// Track progress
let totalDeleted = 0;
let hasMore = true;
let afterCursor = null;
let pageNum = 1;
// Code continues below...
}
What each line does:
projectId: Tells GitLab which project to useprojectPath: Alternative way to identify the projectaccessToken: Your access token for authenticationtotalDeleted: Counts how many artifacts we’ve deletedafterCursor: Remembers where we are in the list (for pagination)
Step 2: Main Loop
The script runs in a loop until all artifacts are deleted:
while (hasMore) {
console.log(`\n=== Processing batch ${pageNum} ===`);
// 1. Fetch jobs
// 2. Get artifact IDs
// 3. Delete artifacts
// 4. Check if more pages exist
}
Step 3: Fetch Jobs
Get 100 jobs at a time using GraphQL:
const queryVariables = {
fullPath: projectPath,
first: 100
};
if (afterCursor) {
queryVariables.after = afterCursor;
}
const queryResponse = await fetch(`${baseUrl}/api/graphql`, {
method: `POST`,
headers: {
"Content-Type": `application/json`,
"Authorization": `Bearer ${accessToken}`
},
body: JSON.stringify({
query: `query($fullPath: ID!, $first: Int!, $after: String) {
project(fullPath: $fullPath) {
jobs(first: $first, after: $after) {
pageInfo {
hasNextPage
endCursor
}
nodes {
id
artifacts {
nodes {
id
}
}
}
}
}
}`,
variables: queryVariables
})
});
How this works:
- Get 100 jobs per request
- Use
afterCursorto start from where we left off - Get artifact IDs from each job
- Get pagination info to know if more jobs exist
About Pagination:
We use cursor-based pagination. Think of it like bookmarks:
- First request: “Give me first 100 jobs” → Get cursor “abc123”
- Second request: “Give me 100 jobs after abc123” → Get cursor “def456”
- Last request: “Give me 100 jobs after xyz789” → No more jobs
This is better than counting (page 1, 2, 3) because if items are deleted while we’re running, we won’t miss anything or get duplicates.
Step 4: Check Response
Always check if the request worked:
const queryResult = await queryResponse.json();
if (queryResult.errors) {
console.error(`Errors:`, queryResult.errors);
break;
}
if (!queryResult.data || !queryResult.data.project) {
console.error(`No data returned`);
break;
}
const jobsData = queryResult.data.project.jobs;
const jobs = jobsData.nodes;
const pageInfo = jobsData.pageInfo;
console.log(`Found ${jobs.length} jobs`);
Step 5: Get Artifact IDs
Loop through jobs and collect artifact IDs:
const artifactIds = [];
for (const job of jobs) {
if (job.artifacts && job.artifacts.nodes) {
for (const artifact of job.artifacts.nodes) {
artifactIds.push(artifact.id);
}
}
}
console.log(`Found ${artifactIds.length} artifacts`);
Each job can have many artifacts. We collect all IDs into one array.
Step 6: Delete in Batches
GitLab lets you delete 20 artifacts at a time. We split our list into groups of 20:
if (artifactIds.length > 0) {
for (let i = 0; i < artifactIds.length; i += 20) {
const batch = artifactIds.slice(i, i + 20);
// Delete this batch (next step)
}
}
Why 20 at a time?
- GitLab’s limit: max 20 per request
- Better progress tracking: see results after each batch
- Safer: if one batch fails, others still work
Step 7: Delete Mutation
Send the delete request:
const deleteResponse = await fetch(`${baseUrl}/api/graphql`, {
method: `POST`,
headers: {
"Content-Type": `application/json`,
"Authorization": `Bearer ${accessToken}`
},
body: JSON.stringify({
operationName: `bulkDestroyJobArtifacts`,
variables: {
projectId: projectId,
ids: batch
},
query: `mutation bulkDestroyJobArtifacts($projectId: ProjectID!, $ids: [CiJobArtifactID!]!) {
bulkDestroyJobArtifacts(input: {projectId: $projectId, ids: $ids}) {
destroyedCount
destroyedIds
errors
}
}`
})
});
This tells GitLab: “Delete these artifacts from this project”.
Step 8: Check Delete Results
See how many were deleted:
const result = await deleteResponse.json();
if (result.data && result.data.bulkDestroyJobArtifacts) {
const count = result.data.bulkDestroyJobArtifacts.destroyedCount || 0;
totalDeleted += count;
console.log(` ✅ Deleted ${count} artifacts (Total: ${totalDeleted})`);
if (result.data.bulkDestroyJobArtifacts.errors?.length > 0) {
console.error(` ❌ Errors:`, result.data.bulkDestroyJobArtifacts.errors);
}
}
// Wait 500ms before next batch (be nice to the server)
await new Promise(resolve => setTimeout(resolve, 500));
Why wait 500ms?
- Prevents overloading GitLab’s servers
- Avoids getting blocked for too many requests
- GitLab can process each request properly
Step 9: Move to Next Page
After deleting all artifacts in this batch of jobs, check if there are more:
if (pageInfo.hasNextPage) {
afterCursor = pageInfo.endCursor;
pageNum++;
console.log(`Moving to next page`);
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
hasMore = false;
console.log(`No more jobs`);
}
Wait 1 second between job pages to be respectful of the API.
=== Batch 1 ===
Found 100 jobs
Found 45 artifacts
✅ Deleted 20 (Total: 20)
✅ Deleted 20 (Total: 40)
✅ Deleted 5 (Total: 45)
=== Batch 2 ===
Found 100 jobs
Found 32 artifacts
✅ Deleted 20 (Total: 65)
✅ Deleted 12 (Total: 77)
🎉 Done! Deleted 77 artifacts
Conclusion
This script automates GitLab artifact deletion using the official GraphQL API. It handles large numbers of artifacts automatically, shows progress, and respects API limits.
You can adapt this for other GitLab tasks like managing pipelines, cleaning branches, or analyzing CI/CD data.
Start the conversation