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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ So it can be used with our [Docker Build Push action](https://github.com/docker/
| `DOCKER_METADATA_SHORT_SHA_LENGTH` | Number | Specifies the length of the [short commit SHA](#typesha) to ensure uniqueness. Default is `7`, but can be increased for larger repositories. |
| `DOCKER_METADATA_ANNOTATIONS_LEVELS` | String | Comma separated list of annotations levels to set for annotations output separated (default `manifest`) |
| `DOCKER_METADATA_SET_OUTPUT_ENV` | Bool | If `true`, sets each output as an environment variable (default `true`) |
| `SOURCE_DATE_EPOCH` | Number | Unix timestamp used for generated `org.opencontainers.image.created` labels and annotations instead of the current time |

## `context` input

Expand Down
49 changes: 48 additions & 1 deletion __tests__/meta.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {beforeEach, describe, expect, test, vi} from 'vitest';
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
Expand Down Expand Up @@ -95,6 +95,53 @@ const tagsLabelsTest = async (name: string, envFile: string, inputs: Inputs, exV
expect(annotations).toEqual(exAnnotations ?? exLabels);
};

describe('SOURCE_DATE_EPOCH', () => {
beforeEach(() => {
delete process.env.SOURCE_DATE_EPOCH;
});

afterEach(() => {
vi.unstubAllEnvs();
delete process.env.SOURCE_DATE_EPOCH;
});

const getMetadataDates = async (): Promise<{date: Date; created: Date}> => {
const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta(getInputs(), await getContext(ContextSource.workflow, toolkit), repo);
return meta as unknown as {date: Date; created: Date};
};

test('uses SOURCE_DATE_EPOCH for the OCI created timestamp', async () => {
vi.stubEnv('SOURCE_DATE_EPOCH', '946782245');
const dates = await getMetadataDates();
expect(dates.created.getTime()).toBe(946782245000);
expect(dates.date.getTime()).not.toBe(946782245000);
});

test('supports the Unix epoch', async () => {
vi.stubEnv('SOURCE_DATE_EPOCH', '0');
expect((await getMetadataDates()).created.getTime()).toBe(0);
});

test('uses the current time when SOURCE_DATE_EPOCH is unset', async () => {
const before = Date.now();
const created = (await getMetadataDates()).created.getTime();
expect(created).toBeGreaterThanOrEqual(before);
expect(created).toBeLessThanOrEqual(Date.now());
});

test.each(['', '-1', '1.5', 'not-a-date'])('rejects invalid SOURCE_DATE_EPOCH value %j', async sourceDateEpoch => {
vi.stubEnv('SOURCE_DATE_EPOCH', sourceDateEpoch);
await expect(getMetadataDates()).rejects.toThrow('SOURCE_DATE_EPOCH is not a valid non-negative integer');
});

test('rejects an out-of-range SOURCE_DATE_EPOCH value', async () => {
vi.stubEnv('SOURCE_DATE_EPOCH', '9007199254740992');
await expect(getMetadataDates()).rejects.toThrow('SOURCE_DATE_EPOCH is out of range');
});
});

describe('null', () => {
// prettier-ignore
// eslint-disable-next-line vitest/expect-expect
Expand Down
2 changes: 1 addition & 1 deletion dist/index.cjs

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions dist/index.cjs.map

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion src/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class Meta {
private readonly tags: tcl.Tag[];
private readonly flavor: fcl.Flavor;
private readonly date: Date;
private readonly created: Date;

constructor(inputs: Inputs, context: Context, repo: GitHubRepo) {
this.inputs = inputs;
Expand All @@ -40,9 +41,29 @@ export class Meta {
this.tags = tcl.Transform(inputs.tags);
this.flavor = fcl.Transform(inputs.flavor);
this.date = new Date();
this.created = Meta.getCreatedDate();
this.version = this.getVersion();
}

private static getCreatedDate(): Date {
const sourceDateEpoch = process.env.SOURCE_DATE_EPOCH;
if (sourceDateEpoch === undefined) {
return new Date();
}

if (!/^\d+$/.test(sourceDateEpoch)) {
throw new Error(`SOURCE_DATE_EPOCH is not a valid non-negative integer: ${sourceDateEpoch}`);
}

const seconds = Number(sourceDateEpoch);
const date = new Date(seconds * 1000);
if (!Number.isSafeInteger(seconds) || Number.isNaN(date.getTime())) {
throw new Error(`SOURCE_DATE_EPOCH is out of range: ${sourceDateEpoch}`);
}

return date;
}

private getVersion(): Version {
let version: Version = {
main: undefined,
Expand Down Expand Up @@ -544,7 +565,7 @@ export class Meta {
`org.opencontainers.image.url=${this.repo.html_url || ''}`,
`org.opencontainers.image.source=${this.repo.html_url || ''}`,
`org.opencontainers.image.version=${this.version.main || ''}`,
`org.opencontainers.image.created=${this.date.toISOString()}`,
`org.opencontainers.image.created=${this.created.toISOString()}`,
`org.opencontainers.image.revision=${this.context.sha || ''}`,
`org.opencontainers.image.licenses=${this.repo.license?.spdx_id || ''}`
];
Expand Down
Loading