Skip to content

[Senior Software Engineer] Airbnb - Brazil #5

[Senior Software Engineer] Airbnb - Brazil

[Senior Software Engineer] Airbnb - Brazil #5

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(', ')}`);
}