name: Proto Version Check on: pull_request: paths: - "**/*.pb.go" jobs: check-proto-versions: runs-on: ubuntu-latest steps: - name: Check for proto tool version changes uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const files = await github.paginate(github.rest.pulls.listFiles, { owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, per_page: 100, }); // Cover renamed .pb.go files in addition to plain edits. // Renamed entries land under the new path with previous_filename // pointing at the base-side name, so we read the base content // from the old path when present. const changedPbFiles = files .filter(f => (f.status === 'modified' || f.status === 'renamed') && f.filename.endsWith('.pb.go')) .map(f => ({ headPath: f.filename, basePath: f.previous_filename || f.filename, })); if (changedPbFiles.length === 0) { console.log('No modified or renamed .pb.go files to check'); return; } // Matches the generator version headers protoc writes at the top // of generated files: // // protoc v3.21.12 // // protoc-gen-go v1.26.0 // // - protoc-gen-go-grpc v1.6.1 (grpc files prefix with "- ") // The optional "- " prefix and the optional -gen-go / -gen-go-grpc // suffixes keep the *_grpc.pb.go headers in scope. const versionPattern = /^\s*\/\/\s+(?:-\s+)?protoc(?:-gen-go(?:-grpc)?)?\s+v[\d.]+/; const baseSha = context.payload.pull_request.base.sha; const headSha = context.payload.pull_request.head.sha; async function getVersionHeader(path, ref) { try { const res = await github.rest.repos.getContent({ owner: context.repo.owner, repo: context.repo.repo, path, ref, }); if (!res.data.content) { return { ok: false, reason: 'no inline content (file too large)' }; } const content = Buffer.from(res.data.content, 'base64').toString('utf8'); const lines = content .split('\n') .slice(0, 20) .filter(line => versionPattern.test(line)); return { ok: true, lines }; } catch (e) { return { ok: false, reason: e.message }; } } const violations = []; for (const file of changedPbFiles) { const [base, head] = await Promise.all([ getVersionHeader(file.basePath, baseSha), getVersionHeader(file.headPath, headSha), ]); if (!base.ok || !head.ok) { core.warning( `Skipping ${file.headPath}: base=${base.ok ? 'ok' : base.reason}, head=${head.ok ? 'ok' : head.reason}` ); continue; } if (base.lines.join('\n') !== head.lines.join('\n')) { violations.push({ file: file.basePath === file.headPath ? file.headPath : `${file.basePath} → ${file.headPath}`, base: base.lines, head: head.lines, }); } } if (violations.length > 0) { const details = violations.map(v => `${v.file}:\n` + ` base:\n${v.base.map(l => ' ' + l).join('\n') || ' (none)'}\n` + ` head:\n${v.head.map(l => ' ' + l).join('\n') || ' (none)'}` ).join('\n\n'); core.setFailed( `Proto version strings changed in generated files.\n` + `This usually means the wrong protoc or protoc-gen-go version was used.\n` + `Regenerate with the matching tool versions.\n\n` + details ); return; } console.log('No proto version string changes detected');