ci: automerge minor and patch app upgrades (#720)

* fix: app version mismatchs between config and docker-compose

* ci: create automerge workflow

wip
This commit is contained in:
Nicolas Meienberger 2023-06-20 22:52:42 +02:00 committed by GitHub
parent c354f1f094
commit e495cc1293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 903 additions and 696 deletions

28
.github/workflows/auto-merge.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: automerge
on:
pull_request:
types:
- labeled
- unlabeled
- synchronize
- opened
- edited
- ready_for_review
- reopened
- unlocked
pull_request_review:
types:
- submitted
check_suite:
types:
- completed
status: {}
jobs:
automerge:
runs-on: ubuntu-latest
steps:
- id: automerge
name: automerge
uses: "pascalgn/automerge-action@v0.15.6"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -1,9 +1,8 @@
name: Tipi CI name: Tipi CI
on: on:
push:
pull_request: pull_request:
types: [opened, synchronize, reopened, ready_for_review] types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -26,7 +25,7 @@ jobs:
- name: Get pnpm store directory - name: Get pnpm store directory
id: pnpm-cache id: pnpm-cache
run: | run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)" echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3 - uses: actions/cache@v3
name: Setup pnpm cache name: Setup pnpm cache
@ -44,3 +43,70 @@ jobs:
- name: Run linter - name: Run linter
run: pnpm run lint run: pnpm run lint
- name: Check bumped version
id: check-bumped-version
uses: actions/github-script@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const semver = require('semver')
const { data } = await github.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number
});
const modifiedFiles = data.map(file => file.filename);
const filesInAppsFolder = modifiedFiles.filter(file => file.includes('apps/'))
if (filesInAppsFolder.length < modifiedFiles.length) {
console.log('Not all files are in apps folder, skipping automerge')
core.setOutput('major_bump', 'true')
return
}
const configs = modifiedFiles.filter(file => file.includes('config.json'))
let majorBump = 'false'
for (const configFile of configs) {
const baseContent = await github.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: configFile,
ref: context.payload.pull_request.base.ref
});
const baseConfig = JSON.parse(Buffer.from(baseContent.data.content, 'base64').toString('utf-8'))
const currentContent = await github.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: configFile,
ref: context.payload.pull_request.head.ref
});
const currentConfig = JSON.parse(Buffer.from(currentContent.data.content, 'base64').toString('utf-8'))
const baseVersion = semver.coerce(baseConfig.version)
const currentVersion = semver.coerce(currentConfig.version)
if (currentVersion?.major > baseVersion?.major) {
console.log('Major bump detected, skipping automerge')
majorBump = 'true'
break
}
}
core.setOutput('major_bump', majorBump)
- name: Label this PR as "automerge" if major_bump is false
if: steps.check-bumped-version.outputs.major_bump == 'false'
uses: actions/github-script@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["automerge"]
})

View File

@ -21,7 +21,7 @@ jobs:
- name: Get list of updated files by the last commit in this branch separated by space - name: Get list of updated files by the last commit in this branch separated by space
id: updated-files id: updated-files
run: | run: |
echo "::set-output name=files::$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} | tr '\n' ' ')" echo "files=$(git diff-tree --no-commit-id --name-only -r ${{ github.sha }} | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Run renovate-app-version.sh on updated files - name: Run renovate-app-version.sh on updated files
run: | run: |

View File

@ -1,5 +1,6 @@
import fs from 'fs'; import fs from "fs";
import jsyaml from 'js-yaml'; import semver from "semver";
import jsyaml from "js-yaml";
interface AppConfig { interface AppConfig {
id: string; id: string;
@ -18,11 +19,18 @@ interface AppConfig {
available: boolean; available: boolean;
} }
const networkExceptions = ['pihole', 'tailscale', 'homeassistant', 'plex', 'zerotier', 'gladys']; const networkExceptions = [
"pihole",
"tailscale",
"homeassistant",
"plex",
"zerotier",
"gladys",
];
const getAppConfigs = (): AppConfig[] => { const getAppConfigs = (): AppConfig[] => {
const apps: AppConfig[] = []; const apps: AppConfig[] = [];
const appsDir = fs.readdirSync('./apps'); const appsDir = fs.readdirSync("./apps");
appsDir.forEach((app: string) => { appsDir.forEach((app: string) => {
const path = `./apps/${app}/config.json`; const path = `./apps/${app}/config.json`;
@ -36,7 +44,7 @@ const getAppConfigs = (): AppConfig[] => {
apps.push(config); apps.push(config);
} }
} catch (e) { } catch (e) {
console.error('Error parsing config file', app); console.error("Error parsing config file", app);
} }
} }
}); });
@ -44,152 +52,198 @@ const getAppConfigs = (): AppConfig[] => {
return apps; return apps;
}; };
describe('App configs', () => { describe("App configs", () => {
it('Get app config should return at least one app', () => { it("Get app config should return at least one app", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
expect(apps.length).toBeGreaterThan(0); expect(apps.length).toBeGreaterThan(0);
}); });
it('Each app should have an id', () => { describe("Each app should have an id", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.id).toBeDefined(); test(app.id, () => {
expect(app.id).toBeDefined();
});
}); });
}); });
it('Each app should have a md description', () => { describe("Each app should have a md description", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
const path = `./apps/${app.id}/metadata/description.md`; test(app.id, () => {
const path = `./apps/${app.id}/metadata/description.md`;
if (fs.existsSync(path)) { if (fs.existsSync(path)) {
const description = fs.readFileSync(path).toString(); const description = fs.readFileSync(path).toString();
expect(description).toBeDefined(); expect(description).toBeDefined();
} else { } else {
console.error(`Missing description for app ${app.id}`); expect(true).toBe(false);
expect(true).toBe(false); }
} });
}); });
}); });
it('Each app should have categories defined as an array', () => { describe("Each app should have categories defined as an array", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.categories).toBeDefined(); test(app.id, () => {
expect(app.categories).toBeInstanceOf(Array); expect(app.categories).toBeDefined();
expect(app.categories).toBeInstanceOf(Array);
});
}); });
}); });
it('Each app should have a name', () => { describe("Each app should have a name", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.name).toBeDefined(); test(app.id, () => {
expect(app.name).toBeDefined();
});
}); });
}); });
it('Each app should have a description', () => { describe("Each app should have a description", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.description).toBeDefined(); test(app.id, () => {
expect(app.description).toBeDefined();
});
}); });
}); });
it('Each app should have a port', () => { describe("Each app should have a port", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.port).toBeDefined(); test(app.id, () => {
expect(app.port).toBeGreaterThan(999); expect(app.port).toBeDefined();
expect(app.port).toBeLessThan(65535); expect(app.port).toBeGreaterThan(999);
expect(app.port).toBeLessThan(65535);
});
}); });
}); });
it('Each app should have a different port', () => { test("Each app should have a different port", () => {
const appConfigs = getAppConfigs(); const appConfigs = getAppConfigs();
const ports = appConfigs.map((app) => app.port); const ports = appConfigs.map((app) => app.port);
expect(new Set(ports).size).toBe(appConfigs.length); expect(new Set(ports).size).toBe(appConfigs.length);
}); });
it('Each app should have a unique id', () => { test("Each app should have a unique id", () => {
const appConfigs = getAppConfigs(); const appConfigs = getAppConfigs();
const ids = appConfigs.map((app) => app.id); const ids = appConfigs.map((app) => app.id);
expect(new Set(ids).size).toBe(appConfigs.length); expect(new Set(ids).size).toBe(appConfigs.length);
}); });
it('Each app should have a version', () => { describe("Each app should have a version", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(app.tipi_version).toBeDefined(); test(app.id, () => {
expect(app.tipi_version).toBeGreaterThan(0); expect(app.version).toBeDefined();
expect(app.tipi_version).toBeDefined();
expect(app.tipi_version).toBeGreaterThan(0);
});
}); });
}); });
it('Each app should have a docker-compose file beside it', () => { describe("Each app should have a docker-compose file beside it", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(fs.existsSync(`./apps/${app.id}/docker-compose.yml`)).toBe(true); test(app.id, () => {
expect(fs.existsSync(`./apps/${app.id}/docker-compose.yml`)).toBe(true);
});
}); });
}); });
it('Each app should have a metadata folder beside it', () => { describe("Each app should have a metadata folder beside it", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(fs.existsSync(`./apps/${app.id}/metadata`)).toBe(true); test(app.id, () => {
expect(fs.existsSync(`./apps/${app.id}/metadata`)).toBe(true);
});
}); });
}); });
it('Each app should have a file named logo.jpg in the metadata folder', () => { describe("Each app should have a file named logo.jpg in the metadata folder", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
expect(fs.existsSync(`./apps/${app.id}/metadata/logo.jpg`)).toBe(true); test(app.id, () => {
expect(fs.existsSync(`./apps/${app.id}/metadata/logo.jpg`)).toBe(true);
});
}); });
}); });
it('Each app should have a container name equals to its id', () => { describe("Each app should have a container name equals to its id", () => {
const apps = getAppConfigs(); const apps = getAppConfigs();
apps.forEach((app) => { apps.forEach((app) => {
const dockerComposeFile = fs.readFileSync(`./apps/${app.id}/docker-compose.yml`).toString(); test(app.id, () => {
const dockerComposeFile = fs
const dockerCompose: any = jsyaml.load(dockerComposeFile); .readFileSync(`./apps/${app.id}/docker-compose.yml`)
.toString();
if (!dockerCompose.services[app.id]) {
console.error(app.id);
}
expect(dockerCompose.services[app.id]).toBeDefined();
expect(dockerCompose.services[app.id].container_name).toBe(app.id);
});
});
it('Each app should have network tipi_main_network', () => {
const apps = getAppConfigs();
apps.forEach((app) => {
if (!networkExceptions.includes(app.id)) {
const dockerComposeFile = fs.readFileSync(`./apps/${app.id}/docker-compose.yml`).toString();
const dockerCompose: any = jsyaml.load(dockerComposeFile); const dockerCompose: any = jsyaml.load(dockerComposeFile);
expect(dockerCompose.services[app.id]).toBeDefined(); expect(dockerCompose.services[app.id]).toBeDefined();
expect(dockerCompose.services[app.id].container_name).toBe(app.id);
});
});
});
if (!dockerCompose.services[app.id].networks) { describe("Each app should have the same version in config.json and docker-compose.yml", () => {
console.error(app.id); const exceptions = ["revolt"];
const apps = getAppConfigs().filter((app) => !exceptions.includes(app.id));
apps.forEach((app) => {
test(app.id, () => {
const dockerComposeFile = fs
.readFileSync(`./apps/${app.id}/docker-compose.yml`)
.toString();
const dockerCompose: any = jsyaml.load(dockerComposeFile);
expect(dockerCompose.services[app.id]).toBeDefined();
expect(dockerCompose.services[app.id].image).toBeDefined();
const dockerImage = dockerCompose.services[app.id].image;
const version = dockerImage.split(":")[1];
expect(version).toContain(app.version);
});
});
});
describe("Each app should have network tipi_main_network", () => {
const apps = getAppConfigs();
apps.forEach((app) => {
test(app.id, () => {
if (!networkExceptions.includes(app.id)) {
const dockerComposeFile = fs
.readFileSync(`./apps/${app.id}/docker-compose.yml`)
.toString();
const dockerCompose: any = jsyaml.load(dockerComposeFile);
expect(dockerCompose.services[app.id]).toBeDefined();
expect(dockerCompose.services[app.id].networks).toBeDefined();
expect(dockerCompose.services[app.id].networks).toStrictEqual([
"tipi_main_network",
]);
} }
});
expect(dockerCompose.services[app.id].networks).toBeDefined();
expect(dockerCompose.services[app.id].networks).toStrictEqual(['tipi_main_network']);
}
}); });
}); });
}); });

View File

@ -4,7 +4,7 @@
"available": true, "available": true,
"exposable": true, "exposable": true,
"tipi_version": 11, "tipi_version": 11,
"version": "0.107.32", "version": "v0.107.32",
"port": 8104, "port": 8104,
"id": "adguard", "id": "adguard",
"categories": [ "categories": [

View File

@ -6,8 +6,10 @@
"exposable": true, "exposable": true,
"id": "autobrr", "id": "autobrr",
"tipi_version": 6, "tipi_version": 6,
"version": "1.26.1", "version": "v1.26.2",
"categories": ["media"], "categories": [
"media"
],
"description": "autobrr is the modern download automation tool for torrents. With inspiration and ideas from tools like trackarr, autodl-irssi and flexget we built one tool that can do it all, and then some.", "description": "autobrr is the modern download automation tool for torrents. With inspiration and ideas from tools like trackarr, autodl-irssi and flexget we built one tool that can do it all, and then some.",
"short_desc": "Automation for downloads.", "short_desc": "Automation for downloads.",
"author": "autobrr", "author": "autobrr",

View File

@ -6,13 +6,17 @@
"exposable": true, "exposable": true,
"id": "chatgpt-ui", "id": "chatgpt-ui",
"tipi_version": 9, "tipi_version": 9,
"version": "2.5.5", "version": "v2.5.5",
"categories": ["ai"], "categories": [
"ai"
],
"description": "A ChatGPT web client that supports multiple users, multiple languages, and multiple database connections for persistent data storage", "description": "A ChatGPT web client that supports multiple users, multiple languages, and multiple database connections for persistent data storage",
"short_desc": "A ChatGPT web client that supports multiple users, multiple languages, and multiple database connections for persistent data storage", "short_desc": "A ChatGPT web client that supports multiple users, multiple languages, and multiple database connections for persistent data storage",
"author": "https://github.com/WongSaang", "author": "https://github.com/WongSaang",
"source": "https://github.com/WongSaang/chatgpt-ui", "source": "https://github.com/WongSaang/chatgpt-ui",
"supported_architectures": ["amd64"], "supported_architectures": [
"amd64"
],
"form_fields": [ "form_fields": [
{ {
"type": "random", "type": "random",

View File

@ -3,7 +3,7 @@ version: "3"
services: services:
deemix: deemix:
container_name: deemix container_name: deemix
image: registry.gitlab.com/bockiii/deemix-docker image: registry.gitlab.com/bockiii/deemix-docker:latest
ports: ports:
- ${APP_PORT}:6595 - ${APP_PORT}:6595
volumes: volumes:

View File

@ -6,8 +6,10 @@
"port": 8000, "port": 8000,
"id": "hello-world", "id": "hello-world",
"tipi_version": 2, "tipi_version": 2,
"version": "1.0.0", "version": "latest",
"categories": ["utilities"], "categories": [
"utilities"
],
"description": "Hello World web server in under 2 MB", "description": "Hello World web server in under 2 MB",
"short_desc": "Hello World web server in under 2 MB", "short_desc": "Hello World web server in under 2 MB",
"author": "crccheck", "author": "crccheck",

View File

@ -2,7 +2,7 @@ version: "3.7"
services: services:
hello-world: hello-world:
container_name: hello-world container_name: hello-world
image: crccheck/hello-world image: crccheck/hello-world:latest
restart: unless-stopped restart: unless-stopped
ports: ports:
- ${APP_PORT}:8000 - ${APP_PORT}:8000

View File

@ -3,7 +3,7 @@ version: "3.7"
services: services:
minecraft-server: minecraft-server:
container_name: minecraft-server container_name: minecraft-server
image: itzg/minecraft-server image: itzg/minecraft-server:latest
ports: ports:
- ${APP_PORT}:25565 - ${APP_PORT}:25565
environment: environment:

View File

@ -6,13 +6,18 @@
"exposable": true, "exposable": true,
"id": "mixpost-pro", "id": "mixpost-pro",
"tipi_version": 6, "tipi_version": 6,
"version": "0.7", "version": "latest",
"categories": ["social", "utilities"], "categories": [
"social",
"utilities"
],
"description": "Mixpost it's the coolest Self-hosted social media management software.", "description": "Mixpost it's the coolest Self-hosted social media management software.",
"short_desc": "Self-hosted social media management. Schedule and organize your social content. ", "short_desc": "Self-hosted social media management. Schedule and organize your social content. ",
"author": "Inovector", "author": "Inovector",
"source": "https://github.com/inovector/mixpost", "source": "https://github.com/inovector/mixpost",
"supported_architectures": ["amd64"], "supported_architectures": [
"amd64"
],
"form_fields": [ "form_fields": [
{ {
"type": "random", "type": "random",

View File

@ -7,8 +7,11 @@
"id": "openbooks", "id": "openbooks",
"tipi_version": 3, "tipi_version": 3,
"url_suffix": "/openbooks/", "url_suffix": "/openbooks/",
"version": "v4.5.0", "version": "4.5.0",
"categories": ["media", "books"], "categories": [
"media",
"books"
],
"description": "Openbooks allows you to download ebooks from irc.irchighway.net quickly and easily. ", "description": "Openbooks allows you to download ebooks from irc.irchighway.net quickly and easily. ",
"short_desc": "Search and Download eBooks", "short_desc": "Search and Download eBooks",
"author": "https://github.com/evan-buss", "author": "https://github.com/evan-buss",

View File

@ -9,8 +9,8 @@
"social" "social"
], ],
"description": "", "description": "",
"tipi_version": 3, "tipi_version": 4,
"version": "1.0.0", "version": "2.4.9.2",
"short_desc": "Open source alternative frontend for TikTok made using PHP ", "short_desc": "Open source alternative frontend for TikTok made using PHP ",
"author": "pablouser1", "author": "pablouser1",
"source": "https://github.com/pablouser1/ProxiTok", "source": "https://github.com/pablouser1/ProxiTok",

View File

@ -3,7 +3,7 @@ services:
proxitok: proxitok:
container_name: proxitok container_name: proxitok
image: ghcr.io/pablouser1/proxitok:master image: ghcr.io/pablouser1/proxitok:v2.4.9.2
restart: unless-stopped restart: unless-stopped
ports: ports:
- ${APP_PORT}:80 - ${APP_PORT}:80

View File

@ -1,10 +1,10 @@
version: '3.7' version: '3.7'
services: services:
vikunja-frontend: vikunja:
depends_on: depends_on:
- vikunja-api - vikunja-api
container_name: vikunja-frontend container_name: vikunja
image: vikunja/frontend:0.20.5 image: vikunja/frontend:0.20.5
restart: unless-stopped restart: unless-stopped
networks: networks:
@ -46,7 +46,7 @@ services:
networks: networks:
- tipi_main_network - tipi_main_network
vikunja: vikunja-proxy:
container_name: vikunja container_name: vikunja
image: nginx image: nginx
ports: ports:

View File

@ -5,12 +5,9 @@
"exposable": true, "exposable": true,
"port": 8103, "port": 8103,
"id": "your-spotify", "id": "your-spotify",
"tipi_version": 3, "tipi_version": 4,
"version": "latest", "version": "1.6.0",
"categories": [ "categories": ["music", "utilities"],
"music",
"utilities"
],
"description": "Self hosted Spotify tracking dashboard.", "description": "Self hosted Spotify tracking dashboard.",
"short_desc": "Self hosted Spotify tracking dashboard.", "short_desc": "Self hosted Spotify tracking dashboard.",
"author": "Yooooomi", "author": "Yooooomi",

View File

@ -3,7 +3,7 @@ version: "3"
services: services:
your-spotify: your-spotify:
container_name: your-spotify container_name: your-spotify
image: yooooomi/your_spotify_client image: yooooomi/your_spotify_client:1.6.0
depends_on: depends_on:
- your-spotify-server - your-spotify-server
restart: unless-stopped restart: unless-stopped

View File

@ -26,6 +26,7 @@
"@types/jest": "^28.1.6", "@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/node": "^18.6.2", "@types/node": "^18.6.2",
"@types/semver": "^7.5.0",
"commitizen": "^4.2.5", "commitizen": "^4.2.5",
"eslint": "^8.22.0", "eslint": "^8.22.0",
"eslint-plugin-json-schema-validator": "^4.0.1", "eslint-plugin-json-schema-validator": "^4.0.1",
@ -33,6 +34,8 @@
"husky": "^8.0.1", "husky": "^8.0.1",
"jest": "^28.1.3", "jest": "^28.1.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"prettier": "^2.8.8",
"semver": "^7.5.2",
"ts-jest": "^28.0.7", "ts-jest": "^28.0.7",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },

File diff suppressed because it is too large Load Diff