Skip to content

Add Docker health check support for container definitions #8

Add Docker health check support for container definitions

Add Docker health check support for container definitions #8

name: Auto-add Container from Issue
on:
issues:
types: [labeled]
jobs:
add-container:
# Only run when the "accepted" label is added to a container submission issue
if: |
github.event.label.name == 'accepted' &&
contains(github.event.issue.labels.*.name, 'container')
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Parse issue body
id: parse
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const body = issue.body;
// Function to extract value from issue form
function extractField(body, fieldId) {
const regex = new RegExp(`### ${fieldId}\\s*([\\s\\S]*?)(?=###|$)`, 'i');
const match = body.match(regex);
return match ? match[1].trim() : '';
}
// Parse the issue body to extract container data
const containerId = extractField(body, 'Container ID');
const name = extractField(body, 'Display Name');
const description = extractField(body, 'Description');
const category = extractField(body, 'Category');
const tags = extractField(body, 'Tags');
const githubUrl = extractField(body, 'GitHub URL');
const icon = extractField(body, 'Icon URL');
const containerData = extractField(body, 'Docker Compose Service Definition');
// Validate required fields
if (!containerId || !name || !description || !category || !tags || !containerData) {
throw new Error('Missing required fields in issue');
}
// Parse tags from comma-separated string
const tagArray = tags ? tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [];
// Determine which file to update based on category
let categoryFile = '';
switch(category.toLowerCase()) {
case 'media':
categoryFile = 'media.ts';
break;
case 'management':
categoryFile = 'management.ts';
break;
case 'database':
categoryFile = 'database.ts';
break;
case 'monitoring':
categoryFile = 'monitoring.ts';
break;
case 'home automation':
categoryFile = 'automation.ts';
break;
default:
categoryFile = 'other.ts';
}
// Store parsed data for next steps
core.setOutput('container_id', containerId);
core.setOutput('name', name);
core.setOutput('description', description);
core.setOutput('category', category);
core.setOutput('tags', JSON.stringify(tagArray));
core.setOutput('github_url', githubUrl);
core.setOutput('icon', icon);
core.setOutput('container_data', containerData);
core.setOutput('category_file', categoryFile);
core.setOutput('issue_number', issue.number);
- name: Create container definition script
run: |
cat > /tmp/add_container.js << 'SCRIPT_EOF'
const fs = require('fs');
// Read outputs from environment variables
const containerId = process.env.CONTAINER_ID;
const name = process.env.CONTAINER_NAME;
const description = process.env.DESCRIPTION;
const category = process.env.CATEGORY;
const tagsJson = process.env.TAGS;
const githubUrl = process.env.GITHUB_URL;
const icon = process.env.ICON;
const containerData = process.env.CONTAINER_DATA;
const categoryFile = process.env.CATEGORY_FILE;
// Parse tags with error handling
let tags;
try {
tags = JSON.parse(tagsJson);
} catch (e) {
console.error('Failed to parse tags:', e);
process.exit(1);
}
// Escape special characters for TypeScript string literals
function escapeString(str) {
return str.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
// Escape template literal content - properly handle ${ expressions
function escapeTemplateLiteral(str) {
return str.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${');
}
// Build the container definition
let containerDef = ' {\n';
containerDef += ` id: "${escapeString(containerId)}",\n`;
containerDef += ` name: "${escapeString(name)}",\n`;
containerDef += ` description: "${escapeString(description)}",\n`;
containerDef += ` category: "${escapeString(category)}",\n`;
containerDef += ` tags: [${tags.map(t => `"${escapeString(t)}"`).join(', ')}],\n`;
if (githubUrl) {
containerDef += ` githubUrl: "${escapeString(githubUrl)}",\n`;
}
if (icon) {
containerDef += ` icon: "${escapeString(icon)}",\n`;
}
containerDef += ` composeContent: \`${escapeTemplateLiteral(containerData)}\`,\n`;
containerDef += ' },\n';
// Read the target file
const filePath = `tools/${categoryFile}`;
// Validate file path to prevent directory traversal
if (categoryFile.includes('..') || categoryFile.includes('/') || !categoryFile.endsWith('.ts')) {
console.error('Invalid category file:', categoryFile);
process.exit(1);
}
let fileContent;
try {
fileContent = fs.readFileSync(filePath, 'utf8');
} catch (e) {
console.error('Failed to read file:', filePath, e);
process.exit(1);
}
// Find the position to insert - look for the export statement closing
// This is more robust than just finding the last ]
const exportMatch = fileContent.match(/export const \w+: DockerTool\[\] = \[/);
if (!exportMatch) {
console.error('Could not find export statement in file');
process.exit(1);
}
// Find the closing bracket after the export statement
const exportStart = exportMatch.index + exportMatch[0].length;
const remainingContent = fileContent.slice(exportStart);
const closingBracketIndex = remainingContent.lastIndexOf(']');
if (closingBracketIndex === -1) {
console.error('Could not find closing bracket in file');
process.exit(1);
}
const insertPosition = exportStart + closingBracketIndex;
// Insert the new container before the closing bracket
const updatedContent = fileContent.slice(0, insertPosition) +
containerDef +
fileContent.slice(insertPosition);
// Write back to file
fs.writeFileSync(filePath, updatedContent, 'utf8');
console.log('Container added successfully to', filePath);
SCRIPT_EOF
- name: Add container to appropriate file
env:
CONTAINER_ID: ${{ steps.parse.outputs.container_id }}
CONTAINER_NAME: ${{ steps.parse.outputs.name }}
DESCRIPTION: ${{ steps.parse.outputs.description }}
CATEGORY: ${{ steps.parse.outputs.category }}
TAGS: ${{ steps.parse.outputs.tags }}
GITHUB_URL: ${{ steps.parse.outputs.github_url }}
ICON: ${{ steps.parse.outputs.icon }}
CONTAINER_DATA: ${{ steps.parse.outputs.container_data }}
CATEGORY_FILE: ${{ steps.parse.outputs.category_file }}
run: |
node /tmp/add_container.js
- name: Install dependencies and test
run: |
bun install
# Run validation tests and capture result
if ! bun test:containers; then
echo "VALIDATION_FAILED=true" >> $GITHUB_ENV
echo "⚠️ Container validation failed. Please review the generated container definition." >> /tmp/validation_message.txt
else
echo "VALIDATION_FAILED=false" >> $GITHUB_ENV
echo "✅ Container validation passed." >> /tmp/validation_message.txt
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Add ${{ steps.parse.outputs.name }} container (from issue #${{ steps.parse.outputs.issue_number }})"
branch: "add-container-${{ steps.parse.outputs.container_id }}"
delete-branch: true
title: "Add ${{ steps.parse.outputs.name }} container"
body: |
## Auto-generated PR from Issue #${{ steps.parse.outputs.issue_number }}
This PR adds the **${{ steps.parse.outputs.name }}** container to the collection.
**Container Details:**
- **ID:** `${{ steps.parse.outputs.container_id }}`
- **Category:** ${{ steps.parse.outputs.category }}
- **Tags:** ${{ steps.parse.outputs.tags }}
${{ steps.parse.outputs.github_url != '' && format('- **GitHub:** {0}', steps.parse.outputs.github_url) || '' }}
**Description:**
${{ steps.parse.outputs.description }}
---
$(cat /tmp/validation_message.txt)
${{ env.VALIDATION_FAILED == 'true' && '⚠️ **Note:** Container validation tests failed. Please review the container definition and make necessary adjustments before merging.' || '' }}
Automatically created from issue #${{ steps.parse.outputs.issue_number }}
labels: |
container
automated
${{ env.VALIDATION_FAILED == 'true' && 'needs-review' || '' }}
- name: Comment on issue
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: ${{ steps.parse.outputs.issue_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ A pull request has been automatically created to add this container! Please review it and make any necessary adjustments.'
});
- name: Handle errors
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '❌ There was an error automatically creating a pull request for this container. Please check the [workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details, or consider contributing directly by following the [CONTRIBUTING.md](https://github.com/${{ github.repository }}/blob/main/CONTRIBUTING.md) guide.'
});