Add Docker health check support for container definitions #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.' | |
| }); |