Skip to content

Commit 8dac770

Browse files
Add experimental squash updates feature
1 parent 8199c85 commit 8dac770

10 files changed

Lines changed: 283 additions & 36 deletions

File tree

README.md

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ It provides a unified interface for managing containerized applications, and aut
2525
- 🌓 **Modern UX**: Automatic light and dark mode (based on system settings)
2626
- 📊 **Job Tracking**: Monitor update jobs with detailed logs and retry capabilities
2727
- 🔐 **Security**: Secure SSH connections, webhook signature verification and OIDC authentication
28+
- 🗜️ **Optional Git Commit Squashing**: Automatically squash multiple dependency update commits to keep Git history clean
2829

2930
## Screenshot
3031

@@ -38,7 +39,7 @@ The app can be started using the following `compose.yml`:
3839
services:
3940
containers-up:
4041
# https://github.com/DigitallyRefined/containers-up/releases
41-
image: ghcr.io/digitallyrefined/containers-up:1.3.8
42+
image: ghcr.io/digitallyrefined/containers-up:1.4.0
4243
restart: unless-stopped
4344
ports:
4445
- 3000:3000
@@ -62,7 +63,7 @@ Optional system wide configuration can be changed by copying `.env.default` to `
6263
services:
6364
containers-up:
6465
# https://github.com/DigitallyRefined/containers-up/releases
65-
image: ghcr.io/digitallyrefined/containers-up:1.3.8
66+
image: ghcr.io/digitallyrefined/containers-up:1.4.0
6667
restart: unless-stopped
6768
volumes:
6869
- ./containers-up/storage:/storage
@@ -215,7 +216,7 @@ on:
215216
- 'renovate/**'
216217
schedule:
217218
# At 02:00, only on Saturday
218-
- cron: "0 2 * * 6"
219+
- cron: '0 2 * * 6'
219220
issues:
220221
types:
221222
- edited
@@ -265,21 +266,24 @@ If everything has been set up correctly the next time Dependabot or Renovate Bot
265266

266267
All environment variables are _optional_ and can be set in the `compose.yml` file via an `env_file: ./.env` or using the `environment:` array. Environment variables starting with `ENV_PUBLIC_` are also embedded in the public HTML output.
267268

268-
| Key | Description | Default |
269-
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- |
270-
| `APP_URL` | App URL to used by links in notifications | |
271-
| `APPRISE_NOTIFICATION` | Apprise is used for container update notifications. See Apprise syntax, see: [Apprise Supported Notifications syntax](https://github.com/caronc/apprise#supported-notifications) | |
272-
| `RUN_COMPOSE_MAX_DEPTH` | How many folders deep to search for compose files | `3` |
273-
| `SSH_CONTROL_PERSIST` | How long SSH connections should persist after last request | `20m` |
274-
| `ENV_PUBLIC_OIDC_ISSUER_URI` | OpenID Connect base URI | Auth disabled |
275-
| `ENV_PUBLIC_OIDC_CLIENT_ID` | OpenID Connect client ID | |
276-
| `OIDC_CLIENT_SECRET` | OpenID Connect client secret | |
277-
| `OIDC_JWKS_URL` | _Optional_ OpenID Connect JSON Web Key Set (file URL) | Auto discovered |
278-
| `MAX_QUEUE_TIME_MINS` | Max time in minutes a queued update can wait for | `10` |
279-
| `LOG_LINES` | Number of previous log lines shown when viewing a containers logs | `500` |
280-
| `DOCKER_USERNAME` | [Docker Hub](https://hub.docker.com) username - used to check for image updates (use if [rate limited by Docker](https://docs.docker.com/docker-hub/usage/)) | |
281-
| `DOCKER_TOKEN` | Docker Hub token | |
282-
| `GHCR_USERNAME` | [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) username - used to check for image updates on GHCR (use if rate limited by GitHub) | |
283-
| `GHCR_TOKEN` | GitHub Container Registry token | |
284-
| `CONTAINER_REGISTRY_USERNAME` | Custom container image registry username | |
285-
| `CONTAINER_REGISTRY_TOKEN` | Custom container image registry token | |
269+
| Key | Description | Default |
270+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
271+
| `APP_URL` | App URL to used by links in notifications | |
272+
| `APPRISE_NOTIFICATION` | Apprise is used for container update notifications. See Apprise syntax, see: [Apprise Supported Notifications syntax](https://github.com/caronc/apprise#supported-notifications) | |
273+
| `RUN_COMPOSE_MAX_DEPTH` | How many folders deep to search for compose files | `3` |
274+
| `SSH_CONTROL_PERSIST` | How long SSH connections should persist after last request | `20m` |
275+
| `ENV_PUBLIC_OIDC_ISSUER_URI` | OpenID Connect base URI | Auth disabled |
276+
| `ENV_PUBLIC_OIDC_CLIENT_ID` | OpenID Connect client ID | |
277+
| `OIDC_CLIENT_SECRET` | OpenID Connect client secret | |
278+
| `OIDC_JWKS_URL` | _Optional_ OpenID Connect JSON Web Key Set (file URL) | Auto discovered |
279+
| `MAX_QUEUE_TIME_MINS` | Max time in minutes a queued update can wait for | `10` |
280+
| `LOG_LINES` | Number of previous log lines shown when viewing a containers logs | `500` |
281+
| `DOCKER_USERNAME` | [Docker Hub](https://hub.docker.com) username - used to check for image updates (use if [rate limited by Docker](https://docs.docker.com/docker-hub/usage/)) | |
282+
| `DOCKER_TOKEN` | Docker Hub token | |
283+
| `GHCR_USERNAME` | [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) username - used to check for image updates on GHCR (use if rate limited by GitHub) | |
284+
| `GHCR_TOKEN` | GitHub Container Registry token | |
285+
| `CONTAINER_REGISTRY_USERNAME` | Custom container image registry username | |
286+
| `CONTAINER_REGISTRY_TOKEN` | Custom container image registry token | |
287+
| `SQUASH_UPDATE_MESSAGE` | Commit message prefix used when squashing dependency update commits (requires squash updates enabled per host) | `Update dependencies` |
288+
| `SQUASH_DAYS_AGO` | Number of days before considering a commit too old to squash with newer dependency updates | `5` |
289+
| `SQUASH_MAX_UPDATE_COMMITS` | Maximum number of dependency update commits to keep before squashing the oldest two together | `5` |

compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
services:
22
containers-up:
33
# https://github.com/DigitallyRefined/containers-up/releases
4-
image: ghcr.io/digitallyrefined/containers-up:1.3.8
4+
image: ghcr.io/digitallyrefined/containers-up:1.4.0
55
container_name: containers-up
66
restart: unless-stopped
77
volumes:

src/backend/db/host.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const host = {
4747
workingFolder,
4848
excludeFolders,
4949
cron,
50+
squashUpdates,
5051
sortOrder,
5152
}: Host) => {
5253
const data = {
@@ -60,6 +61,7 @@ export const host = {
6061
workingFolder,
6162
excludeFolders: excludeFolders || '',
6263
cron,
64+
squashUpdates: squashUpdates ? 1 : 0,
6365
sortOrder,
6466
};
6567

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE host ADD COLUMN squashUpdates INTEGER DEFAULT 0;

src/backend/db/migrations/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ const migrations = [
2222
version: 4,
2323
file: '1.3.2.sql',
2424
},
25+
{
26+
version: 5,
27+
file: '1.4.0.sql',
28+
},
2529
];
2630

2731
export const checkIfDatabaseNeedsUpdating = async () => {

src/backend/db/schema/host.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class Host {
1212
workingFolder?: string;
1313
excludeFolders?: string;
1414
cron?: string;
15+
squashUpdates?: boolean;
1516
sortOrder?: number;
1617
created?: string;
1718
}
@@ -28,6 +29,7 @@ export const hostCreateTableSql = `
2829
workingFolder TEXT,
2930
excludeFolders TEXT,
3031
cron TEXT,
32+
squashUpdates INTEGER DEFAULT 0,
3133
sortOrder INTEGER,
3234
created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
3335
UNIQUE(repoHost, repo)
@@ -65,6 +67,7 @@ export const hostSchema = z.object({
6567
workingFolder: z.string().optional(),
6668
excludeFolders: z.string().optional(),
6769
cron: z.string().optional(),
70+
squashUpdates: z.boolean().optional().default(false),
6871
sortOrder: z.number().optional(),
6972
});
7073

src/backend/endpoints/webhook/pull-restart.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Host } from '@/backend/db/schema/host';
55
import { isComposeFilename } from '@/backend/utils';
66
import { createDockerExec } from '@/backend/utils/docker';
77
import { createExec } from '@/backend/utils/exec';
8+
import { squashUpdates } from '@/backend/utils/git';
89

910
const folderExcluded = (folder: string, excludeFolders: string | null): boolean => {
1011
if (!excludeFolders || excludeFolders === 'null') return false;
@@ -21,25 +22,32 @@ export const pullRestartUpdatedContainers = async (
2122

2223
const exec = createExec(logger);
2324
const dockerExec = createDockerExec(logger);
25+
const sshRun = (cmd: string) => exec.sshRun(name, host, `cd ${workingFolder} && ${cmd}`);
2426

2527
const composePullDownUp = async (composeFolder: string) => {
2628
logger.info(`Restarting services in: ${composeFolder}`);
2729
const response = dockerExec.restartCompose(name, host, composeFolder, true);
2830
await response.text();
2931
};
3032

31-
await exec.sshRun(name, host, `cd ${workingFolder} && git pull --prune`);
33+
const checkAndSquashUpdates = async () => {
34+
if (repoConfig.squashUpdates) {
35+
try {
36+
await squashUpdates(sshRun, logger);
37+
} catch (err) {
38+
logger.error({ err }, 'Failed to squash update commits');
39+
}
40+
}
41+
};
42+
43+
await sshRun(`git pull --prune`);
3244

3345
let composeFolderExists = 'missing';
3446
let containerFolder = '';
3547

3648
if (folder) {
3749
containerFolder = path.join(workingFolder, folder);
38-
const { stdout } = await exec.sshRun(
39-
name,
40-
host,
41-
`test -d "${containerFolder}" && echo exists || echo missing`
42-
);
50+
const { stdout } = await sshRun(`test -d "${containerFolder}" && echo exists || echo missing`);
4351
composeFolderExists = stdout;
4452
}
4553

@@ -49,21 +57,21 @@ export const pullRestartUpdatedContainers = async (
4957
} else {
5058
logger.info(`Compose folder excluded: ${containerFolder}`);
5159
}
60+
await checkAndSquashUpdates();
5261
} else {
5362
// Single compose file watch (via git diff)
54-
const { stdout: changedFilesStdout } = await exec.sshRun(
55-
name,
56-
host,
57-
`cd ${workingFolder} && git diff --name-only HEAD~1 HEAD`
58-
);
63+
const { stdout: changedFilesStdout } = await sshRun(`git diff --name-only HEAD~1 HEAD`);
5964
const changedFiles = changedFilesStdout.split('\n').filter(Boolean);
6065
if (changedFiles.some((f: string) => isComposeFilename(f))) {
6166
for (const changedFile of changedFiles) {
6267
const composeFolder = path.dirname(path.join(workingFolder, changedFile));
6368
if (!folderExcluded(composeFolder, excludeFolders)) {
6469
await composePullDownUp(composeFolder);
70+
} else {
71+
logger.info(`Compose folder excluded: ${composeFolder}`);
6572
}
6673
}
74+
await checkAndSquashUpdates();
6775
} else {
6876
logger.info('No compose YAML files have been changed');
6977
}

0 commit comments

Comments
 (0)