import { resolveCommit, GitSource, UNKNOWN_COMMIT } from '../../src/buildInfo'; // Safe to import: writeBuildInfo guards its side effects behind // require.main === module, so pulling in buildStamp cannot rewrite a stamp. import { buildStamp } from '../../src/writeBuildInfo'; // A source with nothing in it, so each test states only the files it cares // about. Every field is deliberately explicit — a missing `.git` is a normal // case here, not an edge one, and it has to be as easy to write as the others. function source(overrides: Partial = {}): GitSource { return { head: null, readRef: () => null, packedRefs: null, ...overrides }; } const SHA = '846646f7ec8ec0087ec1d89bcee8ab1865b57a7a'; describe('resolveCommit', () => { // What a fresh clone at a specific commit looks like — Portainer checks out // a ref, and a detached HEAD holds the SHA with no indirection at all. it('reads a detached HEAD directly', () => { expect(resolveCommit(source({ head: `${SHA}\n` }))).toBe('846646f'); }); // What a working checkout looks like, including this repository. it('follows a symbolic HEAD to its ref file', () => { expect( resolveCommit( source({ head: 'ref: refs/heads/main\n', readRef: (ref) => (ref === 'refs/heads/main' ? `${SHA}\n` : null) }) ) ).toBe('846646f'); }); // A ref that has been packed has no loose file. Without this the common case // of a freshly cloned repository resolves to unknown. it('falls back to packed-refs when the ref file is absent', () => { expect( resolveCommit( source({ head: 'ref: refs/heads/main\n', packedRefs: `# pack-refs with: peeled fully-peeled sorted\n${SHA} refs/heads/main\n` }) ) ).toBe('846646f'); }); it('picks the right entry out of a packed-refs with many', () => { const other = 'ffffffffffffffffffffffffffffffffffffffff'; expect( resolveCommit( source({ head: 'ref: refs/heads/main\n', packedRefs: `# pack-refs with: peeled fully-peeled sorted\n` + `${other} refs/heads/other\n` + `${SHA} refs/heads/main\n` + `${other} refs/tags/v1\n` }) ) ).toBe('846646f'); }); // Annotated tags write a second `^` line for the object the tag points // at. It is not a ref line and must not be mistaken for one. it('ignores peeled tag lines in packed-refs', () => { expect( resolveCommit( source({ head: 'ref: refs/tags/v1\n', packedRefs: `${SHA} refs/tags/v1\n` + `^ffffffffffffffffffffffffffffffffffffffff\n` }) ) ).toBe('846646f'); }); // Every failure below returns UNKNOWN rather than throwing. This runs during // a Docker build; a version stamp must never be the thing that stops a deploy. it('is unknown when there is no .git at all', () => { expect(resolveCommit(source())).toBe(UNKNOWN_COMMIT); }); it('is unknown when the ref resolves to nothing', () => { expect(resolveCommit(source({ head: 'ref: refs/heads/main\n' }))).toBe(UNKNOWN_COMMIT); }); it('is unknown when packed-refs does not mention the ref', () => { expect( resolveCommit( source({ head: 'ref: refs/heads/gone\n', packedRefs: `${SHA} refs/heads/main\n` }) ) ).toBe(UNKNOWN_COMMIT); }); // A HEAD that is neither a ref line nor a SHA. Returning it verbatim would // put arbitrary file contents on the admin screen. it('is unknown when HEAD is not something it recognises', () => { expect(resolveCommit(source({ head: 'not a sha and not a ref\n' }))).toBe(UNKNOWN_COMMIT); }); it('is unknown when HEAD is empty', () => { expect(resolveCommit(source({ head: ' \n' }))).toBe(UNKNOWN_COMMIT); }); }); describe('buildStamp with a commit passed in', () => { // The half that works where it matters. The Dockerfile does not copy .git — // doing so broke every Portainer deploy (#235) — and building in CI did not // change that (#237), so the builder has to hand the commit over (#248). it('prefers a passed commit over reading .git', () => { expect(buildStamp('/nonexistent/.git', '3085970').commit).toBe('3085970'); }); it('uses a passed commit when there is no .git at all', () => { expect(buildStamp(null, '3085970').commit).toBe('3085970'); }); it('trims a passed commit', () => { expect(buildStamp(null, ' 3085970 ').commit).toBe('3085970'); }); // `--build-arg GIT_COMMIT=` with an unset shell variable expands to this. // Stamping an empty string would read as a commit that happens to be blank // rather than as one nobody supplied. it.each(['', ' '])('treats %p as no commit at all', (passed) => { expect(buildStamp(null, passed).commit).toBe('unknown'); }); it('still says unknown when nothing is passed and there is no .git', () => { expect(buildStamp(null).commit).toBe('unknown'); }); // The property #235 bought and that must not regress: a build with neither // still produces a stamp rather than failing. it('always produces a builtAt', () => { expect(buildStamp(null).builtAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/); }); });