Skip to content

Commit ae62c7c

Browse files
🚀 v1.0
0 parents  commit ae62c7c

File tree

11 files changed

+931
-0
lines changed

11 files changed

+931
-0
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/main.js linguist-generated=true

.github/workflows/test-action.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: 'Test Action'
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
jobs:
10+
run-action:
11+
name: Run action
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
21+
- name: Install dependencies
22+
run: npm ci
23+
24+
- name: Build
25+
run: npm run build
26+
27+
- name: Cleanup Environments (Dry Run)
28+
uses: ./
29+
with:
30+
GITPOD_TOKEN: ${{ secrets.GITPOD_TOKEN }}
31+
ORGANIZATION_ID: ${{ secrets.GITPOD_ORGANIZATION_ID }}
32+
OLDER_THAN_DAYS: '0'
33+
PRINT_SUMMARY: 'true'

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/node_modules
2+
.env
3+
*.log

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM node:20
2+
3+
WORKDIR /action
4+
5+
COPY . .
6+
7+
RUN npm install
8+
RUN npm run build
9+
10+
ENTRYPOINT ["node", "/action/dist/main.js"]

README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Gitpod Environment Cleanup Action
2+
3+
Automatically clean up stopped Gitpod environments that are older than a specified number of days and have no pending changes. This action helps maintain a clean workspace and manage resource usage in your Gitpod Flex organization.
4+
5+
## Features
6+
7+
- 🧹 Cleans up stopped environments automatically
8+
- ⏰ Configurable age threshold for environment deletion (default: 10 days)
9+
- ✅ Only deletes environments with no uncommitted changes or unpushed commits
10+
- 📄 Optional summary report of deleted environments
11+
- 📝 Detailed logging for debugging
12+
- 🔄 Handles pagination for organizations with many environments
13+
14+
## Usage
15+
16+
### Basic Usage
17+
18+
Create a new workflow file (e.g., `.github/workflows/cleanup-gitpod-environments.yml`):
19+
20+
```yaml
21+
name: Cleanup Gitpod Environments
22+
23+
on:
24+
schedule:
25+
- cron: '0 14 * * 6' # Runs at 2:00 PM UTC (14:00) every Saturday
26+
workflow_dispatch: # Allows manual triggering
27+
28+
jobs:
29+
cleanup:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: Cleanup Old Environments
33+
uses: gitpod-io/cleanup-gitpod-environments@v1
34+
with:
35+
GITPOD_TOKEN: ${{ secrets.GITPOD_TOKEN }}
36+
ORGANIZATION_ID: ${{ secrets.GITPOD_ORGANIZATION_ID }}
37+
```
38+
39+
### Advanced Usage
40+
41+
```yaml
42+
name: Cleanup Gitpod Environments
43+
44+
on:
45+
schedule:
46+
- cron: '0 14 * * 6' # Runs at 2:00 PM UTC (14:00) every Saturday
47+
workflow_dispatch: # Allows manual triggering
48+
49+
jobs:
50+
cleanup:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- name: Cleanup Old Environments
54+
uses: gitpod-io/cleanup-gitpod-environments@v1
55+
with:
56+
GITPOD_TOKEN: ${{ secrets.GITPOD_TOKEN }}
57+
ORGANIZATION_ID: ${{ secrets.GITPOD_ORGANIZATION_ID }}
58+
OLDER_THAN_DAYS: 15
59+
PRINT_SUMMARY: true
60+
```
61+
62+
## Inputs
63+
64+
| Input | Required | Default | Description |
65+
|-------|----------|---------|-------------|
66+
| `GITPOD_TOKEN` | Yes | - | Gitpod Personal Access Token with necessary permissions |
67+
| `ORGANIZATION_ID` | Yes | - | Your Gitpod Flex organization ID |
68+
| `OLDER_THAN_DAYS` | No | 10 | Delete environments older than this many days |
69+
| `PRINT_SUMMARY` | No | false | Generate a summary of deleted environments |
70+
71+
## Outputs
72+
73+
| Output | Description |
74+
|--------|-------------|
75+
| `success` | 'true' if the action completed successfully, 'false' otherwise |
76+
| `deleted_count` | Number of environments deleted |
77+
78+
## Prerequisites
79+
80+
1. **Gitpod Personal Access Token**:
81+
- Go to [Gitpod User Settings](https://gitpod.io/user/tokens)
82+
- Create a new token with necessary permissions
83+
- Add it as a GitHub secret named `GITPOD_TOKEN`
84+
85+
2. **Organization ID**:
86+
- Get your organization ID from Gitpod Flex dashboard
87+
- Add it as a GitHub secret named `GITPOD_ORGANIZATION_ID`
88+
89+
## Deletion Criteria
90+
91+
An environment will be deleted if it meets ALL of the following criteria:
92+
- Is in `STOPPED` phase
93+
- Has no uncommitted changes
94+
- Has no unpushed commits
95+
- Is older than the specified number of days
96+
- Belongs to the specified organization
97+
98+
## Security Considerations
99+
100+
- Always store your Gitpod token and organization ID as GitHub secrets
101+
- Review the environments being deleted in the action logs
102+
- Consider starting with a higher `OLDER_THAN_DAYS` value and gradually decreasing it
103+
104+
## Support
105+
106+
For issues and feature requests, please create an issue in this repository.
107+
108+
## Acknowledgments
109+
110+
This action is maintained by [@Siddhant-K-code](https://github.com/Siddhant-K-code) and is not an official Gitpod product.

action.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: 'Gitpod Environment Cleanup'
2+
description: 'Automatically clean up stopped Gitpod environments that are older than a specified number of days and have no pending changes. This action helps maintain a clean workspace and manage resource usage in your Gitpod Flex organization.'
3+
author: 'Siddhant-K-code'
4+
5+
branding:
6+
icon: 'trash-2'
7+
color: 'orange'
8+
9+
inputs:
10+
GITPOD_TOKEN:
11+
description: 'Gitpod Personal Access Token with necessary permissions'
12+
required: true
13+
ORGANIZATION_ID:
14+
description: 'Your Gitpod Flex organization ID'
15+
required: true
16+
OLDER_THAN_DAYS:
17+
description: 'Delete environments older than this many days'
18+
required: false
19+
default: '10'
20+
PRINT_SUMMARY:
21+
description: 'Generate a summary of deleted environments'
22+
required: false
23+
default: 'false'
24+
25+
outputs:
26+
success:
27+
description: 'True if the action completed successfully, false otherwise'
28+
deleted_count:
29+
description: 'Number of environments deleted'
30+
31+
runs:
32+
using: 'docker'
33+
image: 'Dockerfile'

dist/main.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"use strict";
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4+
if (k2 === undefined) k2 = k;
5+
var desc = Object.getOwnPropertyDescriptor(m, k);
6+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7+
desc = { enumerable: true, get: function() { return m[k]; } };
8+
}
9+
Object.defineProperty(o, k2, desc);
10+
}) : (function(o, m, k, k2) {
11+
if (k2 === undefined) k2 = k;
12+
o[k2] = m[k];
13+
}));
14+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15+
Object.defineProperty(o, "default", { enumerable: true, value: v });
16+
}) : function(o, v) {
17+
o["default"] = v;
18+
});
19+
var __importStar = (this && this.__importStar) || function (mod) {
20+
if (mod && mod.__esModule) return mod;
21+
var result = {};
22+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23+
__setModuleDefault(result, mod);
24+
return result;
25+
};
26+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
27+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
28+
return new (P || (P = Promise))(function (resolve, reject) {
29+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
30+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
31+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
32+
step((generator = generator.apply(thisArg, _arguments || [])).next());
33+
});
34+
};
35+
var __importDefault = (this && this.__importDefault) || function (mod) {
36+
return (mod && mod.__esModule) ? mod : { "default": mod };
37+
};
38+
Object.defineProperty(exports, "__esModule", { value: true });
39+
const axios_1 = __importDefault(require("axios"));
40+
const core = __importStar(require("@actions/core"));
41+
/**
42+
* Checks if the given date is older than specified days
43+
*
44+
* @param {string} dateString - ISO date string to check
45+
* @param {number} days - Number of days to compare against
46+
* @returns {boolean} - True if the date is older than specified days
47+
*/
48+
function isOlderThanDays(dateString, days) {
49+
const date = new Date(dateString);
50+
const daysInMs = days * 24 * 60 * 60 * 1000;
51+
const cutoffDate = new Date(Date.now() - daysInMs);
52+
return date < cutoffDate;
53+
}
54+
/**
55+
* Lists the environments from the Gitpod API and identifies those that should be deleted.
56+
* Environments are selected for deletion if they are stopped, do not have changed files
57+
* or unpushed commits, and are older than the specified number of days.
58+
*
59+
* @param {string} gitpodToken - The access token for Gitpod API.
60+
* @param {string} organizationId - The organization ID.
61+
* @param {number} olderThanDays - Delete environments older than these many days
62+
* @returns {Promise<string[]>} - A promise that resolves to an array of environment IDs to be deleted.
63+
*/
64+
function listEnvironments(gitpodToken, organizationId, olderThanDays) {
65+
return __awaiter(this, void 0, void 0, function* () {
66+
const toDelete = [];
67+
let pageToken = undefined;
68+
try {
69+
do {
70+
const response = yield axios_1.default.post("https://app.gitpod.io/api/gitpod.v1.EnvironmentService/ListEnvironments", {
71+
organization_id: organizationId,
72+
pagination: {
73+
page_size: 100,
74+
page_token: pageToken
75+
}
76+
}, {
77+
headers: {
78+
"Content-Type": "application/json",
79+
Authorization: `Bearer ${gitpodToken}`,
80+
},
81+
});
82+
core.debug("API Response: " + JSON.stringify(response.data));
83+
const environments = response.data.environments;
84+
environments.forEach((env) => {
85+
var _a, _b, _c, _d;
86+
const isStopped = env.status.phase === "ENVIRONMENT_PHASE_STOPPED";
87+
const hasNoChangedFiles = !((_b = (_a = env.status.content) === null || _a === void 0 ? void 0 : _a.git) === null || _b === void 0 ? void 0 : _b.totalChangedFiles);
88+
const hasNoUnpushedCommits = !((_d = (_c = env.status.content) === null || _c === void 0 ? void 0 : _c.git) === null || _d === void 0 ? void 0 : _d.totalUnpushedCommits);
89+
const isOldEnough = isOlderThanDays(env.metadata.createdAt, olderThanDays);
90+
if (isStopped && hasNoChangedFiles && hasNoUnpushedCommits && isOldEnough) {
91+
toDelete.push(env.id);
92+
core.debug(`Environment ${env.id} created at ${env.metadata.createdAt} is ${olderThanDays} days old and marked for deletion`);
93+
}
94+
});
95+
pageToken = response.data.pagination.next_page_token;
96+
} while (pageToken); // Continue until no more pages
97+
return toDelete;
98+
}
99+
catch (error) {
100+
core.error(`Error in listEnvironments: ${error}`);
101+
throw error;
102+
}
103+
});
104+
}
105+
/**
106+
* Deletes a specified environment using the Gitpod API.
107+
*
108+
* @param {string} environmentId - The ID of the environment to be deleted.
109+
* @param {string} gitpodToken - The access token for the Gitpod API.
110+
* @param {string} organizationId - The organization ID.
111+
*/
112+
function deleteEnvironment(environmentId, gitpodToken, organizationId) {
113+
return __awaiter(this, void 0, void 0, function* () {
114+
try {
115+
yield axios_1.default.post("https://app.gitpod.io/api/gitpod.v1.EnvironmentService/DeleteEnvironment", {
116+
environment_id: environmentId,
117+
organization_id: organizationId
118+
}, {
119+
headers: {
120+
"Content-Type": "application/json",
121+
Authorization: `Bearer ${gitpodToken}`,
122+
},
123+
});
124+
core.debug(`Deleted environment: ${environmentId}`);
125+
}
126+
catch (error) {
127+
core.error(`Error in deleteEnvironment: ${error}`);
128+
throw error;
129+
}
130+
});
131+
}
132+
/**
133+
* Main function to run the action. It retrieves the Gitpod access token, organization ID,
134+
* and age threshold, lists environments, deletes the selected environments, and outputs the result.
135+
*/
136+
function run() {
137+
return __awaiter(this, void 0, void 0, function* () {
138+
try {
139+
const gitpodToken = core.getInput("GITPOD_TOKEN", { required: true });
140+
const organizationId = core.getInput("ORGANIZATION_ID", { required: true });
141+
const olderThanDays = parseInt(core.getInput("OLDER_THAN_DAYS", { required: false }) || "10");
142+
const printSummary = core.getBooleanInput("PRINT_SUMMARY", { required: false });
143+
const deletedEnvironments = [];
144+
if (!gitpodToken) {
145+
throw new Error("Gitpod access token is required");
146+
}
147+
if (!organizationId) {
148+
throw new Error("Organization ID is required");
149+
}
150+
if (isNaN(olderThanDays) || olderThanDays < 0) {
151+
throw new Error("OLDER_THAN_DAYS must be a positive number");
152+
}
153+
const environmentsToDelete = yield listEnvironments(gitpodToken, organizationId, olderThanDays);
154+
core.info(`Found ${environmentsToDelete.length} environments older than ${olderThanDays} days to delete`);
155+
for (const environmentId of environmentsToDelete) {
156+
yield deleteEnvironment(environmentId, gitpodToken, organizationId);
157+
printSummary ? deletedEnvironments.push(environmentId) : null;
158+
}
159+
if (deletedEnvironments.length > 0 && printSummary) {
160+
core.summary
161+
.addHeading(`Environments deleted (older than ${olderThanDays} days)`)
162+
.addList(deletedEnvironments)
163+
.write();
164+
}
165+
core.setOutput("success", "true");
166+
core.setOutput("deleted_count", deletedEnvironments.length);
167+
}
168+
catch (error) {
169+
core.error(error.message);
170+
core.setOutput("success", "false");
171+
core.setOutput("deleted_count", 0);
172+
}
173+
});
174+
}
175+
run();

0 commit comments

Comments
 (0)