Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ___
* [`{{is_default_branch}}`](#is_default_branch)
* [`{{is_not_default_branch}}`](#is_not_default_branch)
* [`{{date '<format>' tz='<timezone>'}}`](#date-format-tztimezone)
* [`{{commit_count}}`](#commit_count)
* [`{{commit_date '<format>' tz='<timezone>'}}`](#commit_date-format-tztimezone)
* [Major version zero](#major-version-zero)
* [JSON output object](#json-output-object)
Expand Down Expand Up @@ -908,6 +909,26 @@ Default `tz` is UTC.
| `{{date 'dddd, MMMM Do YYYY, h:mm:ss a'}}` | `Friday, January 10th 2020, 3:25:50 pm` |
| `{{date 'YYYYMMDD-HHmmss' tz='Asia/Tokyo'}}` | `20200110-093000` |

#### `{{commit_count}}`

Returns the number of commits reachable from `HEAD` in the selected Git checkout,
using `git rev-list --count HEAD`. Requires `context: git` or `context: git:<path>`;
using this expression with workflow context produces an error.

Shallow checkouts count only the available history. Use `fetch-depth: 0` to fetch
the full history when you need the complete count:

```yaml
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: docker/metadata-action@v6
with:
context: git
images: name/app
tags: type=raw,value=rev-{{commit_count}}
```

#### `{{commit_date '<format>' tz='<timezone>'}}`

Returns the date when the current git commit is committed, rendered by its
Expand Down
18 changes: 18 additions & 0 deletions __tests__/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,15 @@ describe('getContext', () => {
it('workflow does not read Git context', async () => {
const gitContext = vi.spyOn(Git, 'context');
const gitCommitDate = vi.spyOn(Git, 'commitDate');
const gitCommitCount = vi.spyOn(Git, 'commitCount');
const ctx = await context.getContext(context.ContextSource.workflow, toolkit);
expect(ctx.ref).toEqual('refs/heads/dev');
expect(ctx.sha).toEqual('5f3331d7f7044c18ca9f12c77d961c4d7cf3276a');
expect(ctx.commitDate).toEqual(new Date('2024-11-13T13:42:28.000Z'));
expect(gitContext).not.toHaveBeenCalled();
expect(gitCommitDate).not.toHaveBeenCalled();
expect(gitCommitCount).not.toHaveBeenCalled();
expect(ctx.commitCount).toBeUndefined();
});
it.each<[string, string | undefined]>([
['git', undefined],
Expand All @@ -191,9 +194,12 @@ describe('getContext', () => {
vi.spyOn(Git, 'commitDate').mockImplementation(async (): Promise<Date> => {
return new Date('2023-01-01T13:42:28.000Z');
});
vi.spyOn(Git, 'commitCount').mockResolvedValue(42);
const ctx = await context.getContext(source, toolkit);
expect(Git.context).toHaveBeenCalledWith(workdir);
expect(Git.commitDate).toHaveBeenCalledWith('git-test-sha', workdir);
expect(Git.commitCount).toHaveBeenCalledWith(workdir);
expect(ctx.commitCount).toEqual(42);
expect(ctx.ref).toEqual('refs/heads/git-test');
expect(ctx.sha).toEqual('git-test-sha');
expect(ctx.commitDate).toEqual(new Date('2023-01-01T13:42:28.000Z'));
Expand All @@ -218,20 +224,32 @@ describe('getContext', () => {
try {
git(['init', '--initial-branch=selected-checkout']);
git(['-c', 'user.name=Test', '-c', 'user.email=test@example.com', '-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'initial']);
git(['-c', 'user.name=Test', '-c', 'user.email=test@example.com', '-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'second']);
const sha = git(['rev-parse', 'HEAD']);
const ctx = await context.getContext(`git:${workdir}`, toolkit);
expect(ctx.ref).toEqual('refs/heads/selected-checkout');
expect(ctx.sha).toEqual(sha);
expect(ctx.commitDate).toEqual(new Date(commitDate));
expect(ctx.commitCount).toEqual(2);

git(['checkout', '--detach', 'HEAD']);
git(['branch', '-D', 'selected-checkout']);
const detachedContext = await context.getContext(`git:${workdir}`, toolkit);
expect(detachedContext.ref).toEqual('');
expect(detachedContext.sha).toEqual(sha);
expect(detachedContext.commitDate).toEqual(new Date(commitDate));
expect(detachedContext.commitCount).toEqual(2);
const meta = new Meta({...context.getInputs(), images: ['name/app'], tags: ['type=sha,format=long'], flavor: []}, detachedContext, repoFixture);
expect(meta.getTags()).toEqual([`name/app:sha-${sha}`]);

const shallowDir = path.join(checkoutDir, 'shallow');
git(['init', shallowDir]);
git(['-C', shallowDir, 'fetch', '--depth=1', '--no-tags', checkoutDir, sha]);
git(['-C', shallowDir, 'checkout', '--detach', 'FETCH_HEAD']);
const shallowContext = await context.getContext(`git:${shallowDir}`, toolkit);
expect(shallowContext.commitCount).toEqual(1);
const shallowMeta = new Meta({...context.getInputs(), images: ['name/app'], tags: ['type=raw,value=rev-{{commit_count}}'], flavor: []}, shallowContext, repoFixture);
expect(shallowMeta.getTags()).toEqual(['name/app:rev-1']);
} finally {
fs.rmSync(checkoutDir, {recursive: true, force: true});
}
Expand Down
36 changes: 36 additions & 0 deletions __tests__/meta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,42 @@ beforeEach(() => {
});
});

describe('commit_count', () => {
test('expands the count in tags, flavor, labels, and annotations', async () => {
const toolkit = new Toolkit({githubToken: 'fake-github-token'});
const context = await getContext(ContextSource.workflow, toolkit);
context.commitCount = 42;
const meta = new Meta(
{
...getInputs(),
images: ['name/app'],
tags: ['type=raw,value=rev-{{commit_count}}'],
flavor: ['suffix=-{{commit_count}}'],
labels: ['com.example.commit-count={{commit_count}}'],
annotations: ['com.example.commit-count={{commit_count}}']
},
context,
await toolkit.github.repoData()
);
expect(meta.getTags()).toEqual(['name/app:rev-42-42']);
expect(meta.getLabels()).toContain('com.example.commit-count=42');
expect(meta.getAnnotations()).toContain('com.example.commit-count=42');
});

test.each(['tags', 'labels', 'annotations'])('rejects commit_count in %s with workflow context', async input => {
const toolkit = new Toolkit({githubToken: 'fake-github-token'});
const context = await getContext(ContextSource.workflow, toolkit);
const inputs = {...getInputs(), images: ['name/app'], tags: ['type=raw,value=test'], labels: [], annotations: [], flavor: []};
inputs[input] = [input === 'tags' ? 'type=raw,value={{commit_count}}' : 'com.example.commit-count={{commit_count}}'];
const repo = await toolkit.github.repoData();
expect(() => {
const meta = new Meta(inputs, context, repo);
meta.getLabels();
meta.getAnnotations();
}).toThrow('The commit_count expression requires Git context (context: git or git:<path>)');
});
});

describe('isRawStatement', () => {
// prettier-ignore
test.each([
Expand Down
86 changes: 43 additions & 43 deletions dist/index.cjs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.cjs.map

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type GithubContext = typeof GitHub.context;

export interface Context extends GithubContext {
commitDate: Date;
commitCount?: number;
}

export interface Inputs {
Expand Down Expand Up @@ -90,6 +91,7 @@ async function getContextFromGit(workdir?: string): Promise<Context> {

return {
commitDate: await Git.commitDate(ctx.sha, workdir),
commitCount: await Git.commitCount(workdir),
...ctx
} as Context;
}
Expand Down
6 changes: 6 additions & 0 deletions src/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ export class Meta {
sha: function () {
return Meta.shortSha(context.sha);
},
commit_count: function () {
if (context.commitCount === undefined) {
throw new Error('The commit_count expression requires Git context (context: git or git:<path>)');
}
return context.commitCount;
},
base_ref: function () {
if (/^refs\/tags\//.test(context.ref) && context.payload?.base_ref != undefined) {
return context.payload.base_ref.replace(/^refs\/heads\//g, '');
Expand Down