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
on:
push:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
ci:
runs-on: ubuntu-latest
@ -26,7 +25,7 @@ jobs:
- name: Get pnpm store directory
id: pnpm-cache
run: |
echo "::set-output name=pnpm_cache_dir::$(pnpm store path)"
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
name: Setup pnpm cache
@ -44,3 +43,70 @@ jobs:
- name: Run linter
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
id: updated-files
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
run: |

View File

@ -1,5 +1,6 @@
import fs from 'fs';
import jsyaml from 'js-yaml';
import fs from "fs";
import semver from "semver";
import jsyaml from "js-yaml";
interface AppConfig {
id: string;
@ -18,11 +19,18 @@ interface AppConfig {
available: boolean;
}
const networkExceptions = ['pihole', 'tailscale', 'homeassistant', 'plex', 'zerotier', 'gladys'];
const networkExceptions = [
"pihole",
"tailscale",
"homeassistant",
"plex",
"zerotier",
"gladys",
];
const getAppConfigs = (): AppConfig[] => {
const apps: AppConfig[] = [];
const appsDir = fs.readdirSync('./apps');
const appsDir = fs.readdirSync("./apps");
appsDir.forEach((app: string) => {
const path = `./apps/${app}/config.json`;
@ -36,7 +44,7 @@ const getAppConfigs = (): AppConfig[] => {
apps.push(config);
}
} 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;
};
describe('App configs', () => {
it('Get app config should return at least one app', () => {
describe("App configs", () => {
it("Get app config should return at least one app", () => {
const apps = getAppConfigs();
expect(apps.length).toBeGreaterThan(0);
});
it('Each app should have an id', () => {
describe("Each app should have an id", () => {
const apps = getAppConfigs();
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();
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)) {
const description = fs.readFileSync(path).toString();
expect(description).toBeDefined();
} else {
console.error(`Missing description for app ${app.id}`);
expect(true).toBe(false);
}
if (fs.existsSync(path)) {
const description = fs.readFileSync(path).toString();
expect(description).toBeDefined();
} else {
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();
apps.forEach((app) => {
expect(app.categories).toBeDefined();
expect(app.categories).toBeInstanceOf(Array);
test(app.id, () => {
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();
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();
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();
apps.forEach((app) => {
expect(app.port).toBeDefined();
expect(app.port).toBeGreaterThan(999);
expect(app.port).toBeLessThan(65535);
test(app.id, () => {
expect(app.port).toBeDefined();
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 ports = appConfigs.map((app) => app.port);
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 ids = appConfigs.map((app) => app.id);
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();
apps.forEach((app) => {
expect(app.tipi_version).toBeDefined();
expect(app.tipi_version).toBeGreaterThan(0);
test(app.id, () => {
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();
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();
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();
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();
apps.forEach((app) => {
const dockerComposeFile = fs.readFileSync(`./apps/${app.id}/docker-compose.yml`).toString();
const dockerCompose: any = jsyaml.load(dockerComposeFile);
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();
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].container_name).toBe(app.id);
});
});
});
if (!dockerCompose.services[app.id].networks) {
console.error(app.id);
describe("Each app should have the same version in config.json and docker-compose.yml", () => {
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,
"exposable": true,
"tipi_version": 11,
"version": "0.107.32",
"version": "v0.107.32",
"port": 8104,
"id": "adguard",
"categories": [

View File

@ -6,8 +6,10 @@
"exposable": true,
"id": "autobrr",
"tipi_version": 6,
"version": "1.26.1",
"categories": ["media"],
"version": "v1.26.2",
"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.",
"short_desc": "Automation for downloads.",
"author": "autobrr",

View File

@ -6,13 +6,17 @@
"exposable": true,
"id": "chatgpt-ui",
"tipi_version": 9,
"version": "2.5.5",
"categories": ["ai"],
"version": "v2.5.5",
"categories": [
"ai"
],
"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",
"author": "https://github.com/WongSaang",
"source": "https://github.com/WongSaang/chatgpt-ui",
"supported_architectures": ["amd64"],
"supported_architectures": [
"amd64"
],
"form_fields": [
{
"type": "random",

View File

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

View File

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

View File

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

View File

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

View File

@ -6,13 +6,18 @@
"exposable": true,
"id": "mixpost-pro",
"tipi_version": 6,
"version": "0.7",
"categories": ["social", "utilities"],
"version": "latest",
"categories": [
"social",
"utilities"
],
"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. ",
"author": "Inovector",
"source": "https://github.com/inovector/mixpost",
"supported_architectures": ["amd64"],
"supported_architectures": [
"amd64"
],
"form_fields": [
{
"type": "random",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff