[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: Classificar Localização e Salário | |
| on: | |
| issues: | |
| types: ["opened", "edited"] | |
| jobs: | |
| classificar-localizacao: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar labels de localização | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title; | |
| // Estados brasileiros | |
| const estados = { | |
| 'AC': 'Acre', 'AL': 'Alagoas', 'AP': 'Amapá', 'AM': 'Amazonas', | |
| 'BA': 'Bahia', 'CE': 'Ceará', 'DF': 'Distrito Federal', 'ES': 'Espírito Santo', | |
| 'GO': 'Goiás', 'MA': 'Maranhão', 'MT': 'Mato Grosso', 'MS': 'Mato Grosso do Sul', | |
| 'MG': 'Minas Gerais', 'PA': 'Pará', 'PB': 'Paraíba', 'PR': 'Paraná', | |
| 'PE': 'Pernambuco', 'PI': 'Piauí', 'RJ': 'Rio de Janeiro', 'RN': 'Rio Grande do Norte', | |
| 'RS': 'Rio Grande do Sul', 'RO': 'Rondônia', 'RR': 'Roraima', 'SC': 'Santa Catarina', | |
| 'SP': 'São Paulo', 'SE': 'Sergipe', 'TO': 'Tocantins' | |
| }; | |
| // Mapeamento de cidades para estados | |
| const cidadeEstado = { | |
| 'São Paulo': 'SP', 'Campinas': 'SP', | |
| 'Rio de Janeiro': 'RJ', | |
| 'Belo Horizonte': 'MG', 'Uberlândia': 'MG', | |
| 'Brasília': 'DF', | |
| 'Curitiba': 'PR', 'Londrina': 'PR', 'Maringá': 'PR', | |
| 'Porto Alegre': 'RS', | |
| 'Salvador': 'BA', | |
| 'Recife': 'PE', | |
| 'Fortaleza': 'CE', | |
| 'Florianópolis': 'SC', 'Joinville': 'SC', 'Blumenau': 'SC', | |
| 'Goiânia': 'GO', | |
| 'Manaus': 'AM', | |
| 'Vitória': 'ES' | |
| }; | |
| // Extrai apenas o campo de localização do template | |
| const localizacaoMatch = body.match(/###\s*📍\s*Localização\s*\n\n(.+)/i); | |
| const localizacao = localizacaoMatch ? localizacaoMatch[1].trim() : ''; | |
| // Texto restrito: título + campo localização (para siglas) | |
| const textoLocalizacao = (title + ' ' + localizacao).normalize('NFD').replace(/[\u0300-\u036f]/g, ''); | |
| // Texto completo normalizado (para nomes completos de estados/cidades) | |
| const textoCompleto = (title + ' ' + body).normalize('NFD').replace(/[\u0300-\u036f]/g, ''); | |
| const labelsParaAdicionar = new Set(); | |
| // Detecta se é internacional | |
| const internacional = /internacional|global|worldwide|usa|estados unidos|europa|latam|america latina/i; | |
| if (internacional.test(textoLocalizacao)) { | |
| labelsParaAdicionar.add('Internacional'); | |
| } | |
| // Detecta se aceita de qualquer lugar do Brasil | |
| const brasilInteiro = /brasil inteiro|todo brasil|qualquer (lugar|estado|cidade)|anywhere in brazil/i; | |
| if (brasilInteiro.test(textoLocalizacao)) { | |
| labelsParaAdicionar.add('Brasil'); | |
| } | |
| // Detecta estados por SIGLA — apenas no campo localização e título (evita falsos positivos) | |
| for (const [sigla, nome] of Object.entries(estados)) { | |
| const regexSigla = new RegExp(`\\b${sigla}\\b`); | |
| if (regexSigla.test(textoLocalizacao)) { | |
| labelsParaAdicionar.add(sigla); | |
| } | |
| } | |
| // Detecta estados por NOME COMPLETO — no texto inteiro (nomes completos não geram falsos positivos) | |
| for (const [sigla, nome] of Object.entries(estados)) { | |
| const nomeNormalizado = nome.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); | |
| const regexNome = new RegExp(`\\b${nomeNormalizado}\\b`, 'i'); | |
| if (regexNome.test(textoCompleto)) { | |
| labelsParaAdicionar.add(sigla); | |
| } | |
| } | |
| // Detecta cidades importantes — no texto inteiro (nomes de cidades não geram falsos positivos) | |
| for (const [cidade, estado] of Object.entries(cidadeEstado)) { | |
| const cidadeNormalizada = cidade.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); | |
| if (textoCompleto.toLowerCase().includes(cidadeNormalizada.toLowerCase())) { | |
| labelsParaAdicionar.add(estado); | |
| } | |
| } | |
| // Labels de localização válidas para cleanup | |
| const todasLabelsLocalizacao = new Set([ | |
| 'Internacional', 'Brasil', | |
| ...Object.keys(estados) | |
| ]); | |
| // Remove labels de localização antigas antes de adicionar novas | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsLocalizacao.has(label) && !labelsParaAdicionar.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 */ } | |
| } | |
| } | |
| // Adiciona labels | |
| if (labelsParaAdicionar.size > 0) { | |
| const labels = Array.from(labelsParaAdicionar); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: labels | |
| }); | |
| console.log(`✅ Labels de localização adicionadas: ${labels.join(', ')}`); | |
| } | |
| classificar-salario: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar label de faixa salarial | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| // Todas as labels de faixa salarial para cleanup | |
| const todasLabelsSalario = [ | |
| '💰 Até R$ 5k', '💰 R$ 5k - 8k', '💰 R$ 8k - 12k', | |
| '💰 R$ 12k - 16k', '💰 R$ 16k - 20k', '💰 R$ 20k - 25k', | |
| '💰 R$ 25k - 35k', '💰 Acima de R$ 35k', | |
| '💰 Até USD 2k', '💰 USD 2k - 4k', '💰 USD 4k - 6k', | |
| '💰 USD 6k - 8k', '💰 USD 8k - 10k', '💰 USD 10k - 15k', | |
| '💰 Acima de USD 15k', | |
| '💵 Salário Informado' | |
| ]; | |
| // Remove labels de salário antigas | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsSalario.includes(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 */ } | |
| } | |
| } | |
| // Detecta moeda pelo dropdown do template | |
| const moedaMatch = body.match(/###\s*💱\s*Moeda\s*\n\n(.+)/i); | |
| const moeda = moedaMatch ? moedaMatch[1].trim() : ''; | |
| // Busca salário no body (template) | |
| const salarioMatch = body.match(/###\s*💰\s*Faixa Salarial\s*\n\n(.+)/i); | |
| if (!salarioMatch || !salarioMatch[1] || salarioMatch[1].trim() === '_No response_') { | |
| console.log('ℹ️ Faixa salarial não informada'); | |
| return; | |
| } | |
| const salarioTexto = salarioMatch[1].trim(); | |
| // Se é "a combinar", não adiciona label | |
| if (/a combinar|negoci[aá]vel|competitivo/i.test(salarioTexto)) { | |
| console.log('ℹ️ Salário a combinar'); | |
| return; | |
| } | |
| // Determina moeda: prioriza dropdown, depois detecta no texto | |
| let moedaDetectada = 'BRL'; | |
| if (moeda === 'USD' || moeda === 'BRL') { | |
| moedaDetectada = moeda; | |
| } else if (/USD|US\$/.test(salarioTexto) || /(?<![R])\$/.test(salarioTexto)) { | |
| moedaDetectada = 'USD'; | |
| } | |
| // Extrai valores numéricos do texto | |
| const extrairValores = (texto) => { | |
| const valores = []; | |
| let match; | |
| // Padrão com prefixo monetário: R$ 8.000, USD 3000, US$ 3000, $ 3,000 | |
| const regexMoeda = /(?:R\$|USD|US\$|\$)\s*([\d.,]+)/gi; | |
| while ((match = regexMoeda.exec(texto)) !== null) { | |
| let raw = match[1]; | |
| // Formato BR: 8.000 ou 8.000,00 (ponto = milhares, vírgula = decimal) | |
| // Formato US: 8,000 ou 8,000.00 (vírgula = milhares, ponto = decimal) | |
| let valor; | |
| if (raw.includes(',') && raw.indexOf(',') > raw.lastIndexOf('.')) { | |
| // Formato BR: 8.000,00 | |
| valor = parseFloat(raw.replace(/\./g, '').replace(',', '.')); | |
| } else if (raw.includes('.') && raw.indexOf('.') > raw.lastIndexOf(',')) { | |
| // Formato US: 8,000.00 | |
| valor = parseFloat(raw.replace(/,/g, '')); | |
| } else if (raw.includes('.')) { | |
| // Apenas pontos: 8.000 (assume BR milhares) | |
| valor = parseFloat(raw.replace(/\./g, '')); | |
| } else if (raw.includes(',')) { | |
| // Apenas vírgulas: 8,000 (assume US milhares) | |
| valor = parseFloat(raw.replace(/,/g, '')); | |
| } else { | |
| valor = parseFloat(raw); | |
| } | |
| if (!isNaN(valor) && valor > 0) valores.push(valor); | |
| } | |
| // Fallback: padrão numérico simples sem prefixo | |
| if (valores.length === 0) { | |
| const regexNum = /(\d[\d.,]*\d)/g; | |
| while ((match = regexNum.exec(texto)) !== null) { | |
| let raw = match[1]; | |
| let valor; | |
| if (raw.includes(',') && raw.indexOf(',') > raw.lastIndexOf('.')) { | |
| valor = parseFloat(raw.replace(/\./g, '').replace(',', '.')); | |
| } else { | |
| valor = parseFloat(raw.replace(/[.,]/g, '')); | |
| } | |
| if (!isNaN(valor) && valor >= 1000) valores.push(valor); | |
| } | |
| } | |
| return valores; | |
| }; | |
| const valores = extrairValores(salarioTexto); | |
| if (valores.length === 0) { | |
| console.log('ℹ️ Não foi possível extrair valores do salário'); | |
| return; | |
| } | |
| // Pega o maior valor (teto da faixa) | |
| const maiorValor = Math.max(...valores); | |
| // Define faixas salariais por moeda | |
| let labelSalario; | |
| if (moedaDetectada === 'USD') { | |
| if (maiorValor <= 2000) { | |
| labelSalario = '💰 Até USD 2k'; | |
| } else if (maiorValor <= 4000) { | |
| labelSalario = '💰 USD 2k - 4k'; | |
| } else if (maiorValor <= 6000) { | |
| labelSalario = '💰 USD 4k - 6k'; | |
| } else if (maiorValor <= 8000) { | |
| labelSalario = '💰 USD 6k - 8k'; | |
| } else if (maiorValor <= 10000) { | |
| labelSalario = '💰 USD 8k - 10k'; | |
| } else if (maiorValor <= 15000) { | |
| labelSalario = '💰 USD 10k - 15k'; | |
| } else { | |
| labelSalario = '💰 Acima de USD 15k'; | |
| } | |
| } else { | |
| if (maiorValor <= 5000) { | |
| labelSalario = '💰 Até R$ 5k'; | |
| } else if (maiorValor <= 8000) { | |
| labelSalario = '💰 R$ 5k - 8k'; | |
| } else if (maiorValor <= 12000) { | |
| labelSalario = '💰 R$ 8k - 12k'; | |
| } else if (maiorValor <= 16000) { | |
| labelSalario = '💰 R$ 12k - 16k'; | |
| } else if (maiorValor <= 20000) { | |
| labelSalario = '💰 R$ 16k - 20k'; | |
| } else if (maiorValor <= 25000) { | |
| labelSalario = '💰 R$ 20k - 25k'; | |
| } else if (maiorValor <= 35000) { | |
| labelSalario = '💰 R$ 25k - 35k'; | |
| } else { | |
| labelSalario = '💰 Acima de R$ 35k'; | |
| } | |
| } | |
| const labels = [labelSalario, '💵 Salário Informado']; | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: labels | |
| }); | |
| console.log(`✅ Labels de salário adicionadas: ${labels.join(', ')}`); | |
| console.log(`ℹ️ Moeda: ${moedaDetectada} | Maior valor: ${maiorValor.toLocaleString()}`); | |
| classificar-area: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Adicionar labels de área/tecnologia | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const body = issue.body || ''; | |
| const title = issue.title.toLowerCase(); | |
| const texto = (title + ' ' + body).toLowerCase(); | |
| const labelsParaAdicionar = new Set(); | |
| // Áreas de atuação | |
| const areas = { | |
| 'DevOps': /devops|dev\s*ops/i, | |
| 'SRE': /\bsre\b|site reliability|reliability engineer/i, | |
| 'Platform': /platform engineer|plataforma/i, | |
| 'Cloud': /cloud engineer|cloud architect|engenheiro de cloud/i, | |
| 'Infra': /infraestrutura|infrastructure|sysadmin|system admin/i, | |
| 'Security': /devsecops|security|segurança|appsec/i, | |
| 'Data': /data engineer|engenheiro de dados|dataops/i, | |
| 'FinOps': /finops|cloud cost|custo cloud/i | |
| }; | |
| for (const [label, regex] of Object.entries(areas)) { | |
| if (regex.test(texto)) { | |
| labelsParaAdicionar.add(label); | |
| } | |
| } | |
| // Tecnologias principais | |
| const tecnologias = { | |
| 'AWS': /\baws\b|amazon web services/i, | |
| 'Azure': /\bazure\b|microsoft azure/i, | |
| 'GCP': /\bgcp\b|google cloud|gke/i, | |
| 'Kubernetes': /kubernetes|k8s|\beks\b|\baks\b|\bgke\b/i, | |
| 'Terraform': /terraform/i, | |
| 'Docker': /\bdocker\b/i | |
| }; | |
| for (const [label, regex] of Object.entries(tecnologias)) { | |
| if (regex.test(texto)) { | |
| labelsParaAdicionar.add(label); | |
| } | |
| } | |
| // Labels de área/tecnologia válidas para cleanup | |
| const todasLabelsAreaTech = new Set([ | |
| ...Object.keys(areas), | |
| ...Object.keys(tecnologias) | |
| ]); | |
| // Remove labels de área/tech antigas antes de adicionar novas | |
| const labelsAtuais = issue.labels.map(l => l.name); | |
| for (const label of labelsAtuais) { | |
| if (todasLabelsAreaTech.has(label) && !labelsParaAdicionar.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 */ } | |
| } | |
| } | |
| // Adiciona labels | |
| if (labelsParaAdicionar.size > 0) { | |
| const labels = Array.from(labelsParaAdicionar); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: labels | |
| }); | |
| console.log(`✅ Labels de área/tecnologia adicionadas: ${labels.join(', ')}`); | |
| } |