[Senior Software Engineer] Airbnb - Brazil #5
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: Validar e Classificar Vaga | |
| on: | |
| issues: | |
| types: ["opened", "edited"] | |
| jobs: | |
| validar-vaga: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Verificar campos obrigatórios | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title || ''; | |
| // Lista de campos obrigatórios do template | |
| const camposObrigatorios = [ | |
| { nome: 'Cargo', regex: /###\s*🎯\s*Cargo\s*\n\n(.+)/i }, | |
| { nome: 'Empresa', regex: /###\s*🏢\s*Empresa\s*\n\n(.+)/i }, | |
| { nome: 'Nível', regex: /###\s*📊\s*Nível de Experiência\s*\n\n(.+)/i }, | |
| { nome: 'Modalidade', regex: /###\s*🏠\s*Modalidade de Trabalho\s*\n\n(.+)/i }, | |
| { nome: 'Localização', regex: /###\s*📍\s*Localização\s*\n\n(.+)/i }, | |
| { nome: 'Contratação', regex: /###\s*📝\s*Tipo de Contratação\s*\n\n(.+)/i }, | |
| { nome: 'Descrição', regex: /###\s*📋\s*Descrição da Vaga\s*\n\n([\s\S]+?)(?=###|$)/i }, | |
| { nome: 'Requisitos', regex: /###\s*✅\s*Requisitos Técnicos\s*\n\n([\s\S]+?)(?=###|$)/i }, | |
| { nome: 'Como Candidatar', regex: /###\s*📧\s*Como se Candidatar\s*\n\n([\s\S]+?)(?=###|$)/i } | |
| ]; | |
| const camposFaltando = []; | |
| const camposPreenchidos = {}; | |
| for (const campo of camposObrigatorios) { | |
| const match = body.match(campo.regex); | |
| if (!match || !match[1] || match[1].trim() === '' || match[1].trim() === '_No response_') { | |
| camposFaltando.push(campo.nome); | |
| } else { | |
| camposPreenchidos[campo.nome] = match[1].trim(); | |
| } | |
| } | |
| // Marcador para identificar comentários do bot | |
| const marcadorBot = '<!-- bot-validacao -->'; | |
| // Busca comentário existente do bot | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number | |
| }); | |
| const comentarioBot = comments.data.find(c => c.body.includes(marcadorBot)); | |
| // Se faltam campos, cria ou atualiza comentário e label | |
| if (camposFaltando.length > 0) { | |
| const mensagem = `${marcadorBot} | |
| ## ⚠️ Informações Pendentes | |
| Olá! Obrigado por publicar sua vaga no **DevOps Brasil Vagas**! 🚀 | |
| Para que sua vaga seja melhor indexada e encontrada pelos candidatos, por favor complete os seguintes campos: | |
| ${camposFaltando.map(c => `- ❌ **${c}**`).join('\n')} | |
| Por favor, edite a issue e preencha os campos faltantes usando o [template de vagas](https://github.com/DevOps-Brasil/Vagas/issues/new/choose). | |
| --- | |
| *Esta é uma mensagem automática do bot de validação.*`; | |
| if (comentarioBot) { | |
| // Atualiza comentário existente | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: comentarioBot.id, | |
| body: mensagem | |
| }); | |
| } else { | |
| // Cria novo comentário | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: mensagem | |
| }); | |
| } | |
| // Adiciona label de pendente | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: ['Pendente de informações'] | |
| }); | |
| return; | |
| } | |
| // Tudo OK — remove comentário do bot se existir | |
| if (comentarioBot) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: comentarioBot.id | |
| }); | |
| } | |
| // Remove label de pendente se existir | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: 'Pendente de informações' | |
| }); | |
| } catch (e) { | |
| // Label não existe, ignorar | |
| } | |
| console.log('✅ Vaga validada com sucesso!'); | |
| console.log('Campos preenchidos:', camposPreenchidos); | |
| classificar-nivel: | |
| runs-on: ubuntu-latest | |
| needs: validar-vaga | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar label de nível | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title.toLowerCase(); | |
| // Mapeamento de níveis | |
| const niveis = { | |
| 'Estágio': 'Estágio', | |
| 'Júnior': 'Júnior', | |
| 'Pleno': 'Pleno', | |
| 'Sênior': 'Sênior', | |
| 'Especialista/Staff': 'Especialista', | |
| 'Tech Lead': 'Tech Lead', | |
| 'Coordenação/Gerência': 'Gestão' | |
| }; | |
| const todasLabelsNivel = new Set(Object.values(niveis)); | |
| // Busca nível no body (template) | |
| const nivelMatch = body.match(/###\s*📊\s*Nível de Experiência\s*\n\n(.+)/i); | |
| let labelNivel = null; | |
| if (nivelMatch && nivelMatch[1]) { | |
| const nivelSelecionado = nivelMatch[1].trim(); | |
| labelNivel = niveis[nivelSelecionado]; | |
| } | |
| // Fallback: busca no título | |
| if (!labelNivel) { | |
| if (title.includes('junior') || title.includes('júnior') || title.includes('jr')) { | |
| labelNivel = 'Júnior'; | |
| } else if (title.includes('pleno') || title.includes('mid')) { | |
| labelNivel = 'Pleno'; | |
| } else if (title.includes('senior') || title.includes('sênior') || title.includes('sr')) { | |
| labelNivel = 'Sênior'; | |
| } else if (title.includes('lead') || title.includes('líder')) { | |
| labelNivel = 'Tech Lead'; | |
| } else if (title.includes('estag') || title.includes('intern')) { | |
| labelNivel = 'Estágio'; | |
| } | |
| } | |
| // Remove labels de nível antigas | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsNivel.has(label) && label !== labelNivel) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: label | |
| }); | |
| } catch (e) { /* label não existe */ } | |
| } | |
| } | |
| if (labelNivel) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: [labelNivel] | |
| }); | |
| console.log(`✅ Label de nível adicionada: ${labelNivel}`); | |
| } | |
| classificar-contratacao: | |
| runs-on: ubuntu-latest | |
| needs: validar-vaga | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar label de contratação | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title.toLowerCase(); | |
| // Mapeamento de contratação | |
| const contratacoes = { | |
| 'CLT': 'CLT', | |
| 'PJ': 'PJ', | |
| 'CLT ou PJ': ['CLT', 'PJ'], | |
| 'Cooperado': 'Cooperado', | |
| 'Freelancer': 'Freelancer', | |
| 'Estágio': 'Estágio' | |
| }; | |
| const todasLabelsContratacao = new Set(['CLT', 'PJ', 'Cooperado', 'Freelancer', 'Estágio']); | |
| // Busca contratação no body (template) | |
| const contratacaoMatch = body.match(/###\s*📝\s*Tipo de Contratação\s*\n\n(.+)/i); | |
| let labels = []; | |
| if (contratacaoMatch && contratacaoMatch[1]) { | |
| const contratacaoSelecionada = contratacaoMatch[1].trim(); | |
| const label = contratacoes[contratacaoSelecionada]; | |
| if (Array.isArray(label)) { | |
| labels = label; | |
| } else if (label) { | |
| labels = [label]; | |
| } | |
| } | |
| // Fallback: busca no título/body | |
| if (labels.length === 0) { | |
| const texto = (title + ' ' + body).toLowerCase(); | |
| if (texto.includes('clt')) labels.push('CLT'); | |
| if (texto.includes('pj') || texto.includes('pessoa jurídica')) labels.push('PJ'); | |
| } | |
| // Remove labels de contratação antigas | |
| const labelsNovas = new Set(labels); | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsContratacao.has(label) && !labelsNovas.has(label)) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: label | |
| }); | |
| } catch (e) { /* label não existe */ } | |
| } | |
| } | |
| if (labels.length > 0) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: labels | |
| }); | |
| console.log(`✅ Labels de contratação adicionadas: ${labels.join(', ')}`); | |
| } | |
| classificar-modalidade: | |
| runs-on: ubuntu-latest | |
| needs: validar-vaga | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar label de modalidade | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title.toLowerCase(); | |
| // Mapeamento de modalidade | |
| const modalidades = { | |
| 'Remoto': 'Remoto', | |
| 'Híbrido': 'Híbrido', | |
| 'Presencial': 'Presencial' | |
| }; | |
| const todasLabelsModalidade = new Set(Object.values(modalidades)); | |
| // Busca modalidade no body (template) | |
| const modalidadeMatch = body.match(/###\s*🏠\s*Modalidade de Trabalho\s*\n\n(.+)/i); | |
| let labelModalidade = null; | |
| if (modalidadeMatch && modalidadeMatch[1]) { | |
| const modalidadeSelecionada = modalidadeMatch[1].trim(); | |
| labelModalidade = modalidades[modalidadeSelecionada]; | |
| } | |
| // Fallback: busca no título | |
| if (!labelModalidade) { | |
| if (title.includes('remoto') || title.includes('remote')) { | |
| labelModalidade = 'Remoto'; | |
| } else if (title.includes('híbrido') || title.includes('hibrido') || title.includes('hybrid')) { | |
| labelModalidade = 'Híbrido'; | |
| } else if (title.includes('presencial') || title.includes('on-site') || title.includes('onsite')) { | |
| labelModalidade = 'Presencial'; | |
| } | |
| } | |
| // Remove labels de modalidade antigas | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsModalidade.has(label) && label !== labelModalidade) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: label | |
| }); | |
| } catch (e) { /* label não existe */ } | |
| } | |
| } | |
| if (labelModalidade) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: [labelModalidade] | |
| }); | |
| console.log(`✅ Label de modalidade adicionada: ${labelModalidade}`); | |
| } |