[CONTRIBUTION] Update fsqc container #182
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: Process Contribution Issues | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| on: | |
| issues: | |
| types: [opened] | |
| jobs: | |
| process-contribution: | |
| if: contains(github.event.issue.title, '[CONTRIBUTION]') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Install dependencies | |
| run: npm install js-yaml pako | |
| - name: Parse base64 encoded deflate compressed YAML from issue body | |
| id: parse-yaml | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const yaml = require('js-yaml'); | |
| const pako = require('pako'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const issueBody = context.payload.issue.body; | |
| // Extract base64 encoded content from issue body | |
| const base64Regex = /```base64\n([\s\S]*?)\n```/; | |
| const match = issueBody.match(base64Regex); | |
| if (!match) { | |
| core.setFailed('No base64 encoded content found in issue body'); | |
| return; | |
| } | |
| try { | |
| // Decode base64 | |
| const base64Content = match[1].trim(); | |
| const compressedBuffer = Buffer.from(base64Content, 'base64'); | |
| // Decompress using pako (deflate) | |
| const decompressed = pako.inflate(compressedBuffer, { to: 'string' }); | |
| const yamlContent = decompressed; | |
| // Parse YAML to validate and get name | |
| const parsedYaml = yaml.load(yamlContent); | |
| if (!parsedYaml.name) { | |
| core.setFailed('YAML must contain a "name" field'); | |
| return; | |
| } | |
| // Sanitize the name: allow lowercase letters and digits only | |
| const rawName = String(parsedYaml.name); | |
| const normalized = rawName.normalize('NFKD').toLowerCase(); | |
| if (!normalized.match(/^[a-z0-9]+$/)) { | |
| core.setFailed('Invalid recipe name after sanitization: must contain only lowercase letters and numbers'); | |
| return; | |
| } | |
| // Ensure the path stays within the recipes directory | |
| const recipesRoot = path.resolve('recipes'); | |
| const recipeDir = path.resolve(recipesRoot, normalized); | |
| if (!recipeDir.startsWith(recipesRoot + path.sep)) { | |
| core.setFailed('Recipe path escapes the recipes directory'); | |
| return; | |
| } | |
| // Create recipe directory and write file directly | |
| fs.mkdirSync(recipeDir, { recursive: true }); | |
| fs.writeFileSync(path.join(recipeDir, 'build.yaml'), yamlContent); | |
| // Expose sanitized name | |
| core.setOutput('name', normalized); | |
| core.setOutput('parsed', JSON.stringify({ ...parsedYaml, name: normalized })); | |
| } catch (error) { | |
| core.setFailed(`Failed to process encoded content: ${error.message}`); | |
| } | |
| - name: Create branch name | |
| id: create-branch | |
| run: | | |
| set -euo pipefail | |
| BRANCH_NAME="contribution-${{ github.event.issue.number }}-${{ steps.parse-yaml.outputs.name }}" | |
| # Safely write multiline or special characters to GITHUB_OUTPUT | |
| { | |
| echo 'branch_name<<EOF' | |
| echo "$BRANCH_NAME" | |
| echo 'EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Create new branch | |
| run: | | |
| git config --local user.email "[email protected]" | |
| git config --local user.name "GitHub Action" | |
| git checkout -b "${{ steps.create-branch.outputs.branch_name }}" | |
| - name: Check if file exists | |
| id: check-file | |
| run: | | |
| set -euo pipefail | |
| FILE_PATH="recipes/${{ steps.parse-yaml.outputs.name }}/build.yaml" | |
| if [ -f "$FILE_PATH" ]; then | |
| echo "file_exists=true" >> "$GITHUB_OUTPUT" | |
| { echo 'file_path<<EOF'; echo "$FILE_PATH"; echo 'EOF'; } >> "$GITHUB_OUTPUT" | |
| echo "action=update" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "file_exists=false" >> "$GITHUB_OUTPUT" | |
| { echo 'file_path<<EOF'; echo "$FILE_PATH"; echo 'EOF'; } >> "$GITHUB_OUTPUT" | |
| echo "action=add" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Commit changes | |
| run: | | |
| set -euo pipefail | |
| git add recipes/ | |
| # Prepare commit message safely | |
| cat > commitmsg.txt <<'EOF' | |
| ${{ steps.check-file.outputs.action == 'add' && 'Add' || 'Update' }} recipe for ${{ steps.parse-yaml.outputs.name }} from issue #${{ github.event.issue.number }} | |
| EOF | |
| AUTHOR="${{ github.event.issue.user.login }} <${{ github.event.issue.user.id }}+${{ github.event.issue.user.login }}@users.noreply.github.com>" | |
| git commit --author="$AUTHOR" -F commitmsg.txt | |
| - name: Push branch | |
| run: | | |
| set -euo pipefail | |
| git push origin "${{ steps.create-branch.outputs.branch_name }}" | |
| - name: Create pull request | |
| id: create-pr | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: pr } = await github.rest.pulls.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `${{ steps.check-file.outputs.action == 'add' && 'Add' || 'Update' }} recipe for ${{ steps.parse-yaml.outputs.name }}`, | |
| body: `This PR was automatically generated from issue #${{ github.event.issue.number }}. | |
| ## Summary | |
| - ${{ steps.check-file.outputs.action == 'add' && 'Adds new' || 'Updates existing' }} recipe for \`${{ steps.parse-yaml.outputs.name }}\` | |
| - Recipe file: \`${{ steps.check-file.outputs.file_path }}\` | |
| Closes #${{ github.event.issue.number }}`, | |
| head: '${{ steps.create-branch.outputs.branch_name }}', | |
| base: 'main' | |
| }); | |
| core.setOutput('pr_number', pr.number); | |
| core.setOutput('pr_url', pr.html_url); | |
| - name: Comment on issue with PR link | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `✅ A pull request has been automatically created for your contribution! | |
| 🔗 Pull Request: #${{ steps.create-pr.outputs.pr_number }} | |
| The recipe for \`${{ steps.parse-yaml.outputs.name }}\` has been ${{ steps.check-file.outputs.action == 'add' && 'added' || 'updated' }} in the PR. A maintainer will review and merge it soon. | |
| Thank you for your contribution! 🎉` | |
| }); |