mirror of
https://github.com/docker/metadata-action.git
synced 2026-01-21 22:08:58 +08:00
feat: enable customization of annotations+labels
Signed-off-by: Brendon Smith <bws@bws.bio>
This commit is contained in:
parent
ed95091677
commit
b0b9842728
4 changed files with 320 additions and 100 deletions
221
README.md
221
README.md
|
|
@ -34,6 +34,11 @@ ___
|
|||
* [`type=ref`](#typeref)
|
||||
* [`type=raw`](#typeraw)
|
||||
* [`type=sha`](#typesha)
|
||||
* [`annotations` and `labels` inputs](#annotations-and-labels-inputs)
|
||||
* [Default labels and annotations](#default-labels-and-annotations)
|
||||
* [Customize labels and annotations](#customize-labels-and-annotations)
|
||||
* [Annotation outputs](#annotation-outputs)
|
||||
* [Annotation levels](#annotation-levels)
|
||||
* [Notes](#notes)
|
||||
* [Image name and tag sanitization](#image-name-and-tag-sanitization)
|
||||
* [Latest tag](#latest-tag)
|
||||
|
|
@ -49,8 +54,6 @@ ___
|
|||
* [`{{commit_date '<format>' tz='<timezone>'}}`](#commit_date-format-tztimezone)
|
||||
* [Major version zero](#major-version-zero)
|
||||
* [JSON output object](#json-output-object)
|
||||
* [Overwrite labels and annotations](#overwrite-labels-and-annotations)
|
||||
* [Annotations](#annotations)
|
||||
* [Contributing](#contributing)
|
||||
|
||||
## Usage
|
||||
|
|
@ -745,6 +748,128 @@ tags: |
|
|||
type=sha,enable=true,priority=100,prefix=sha-,suffix=,format=short
|
||||
```
|
||||
|
||||
## `annotations` and `labels` inputs
|
||||
|
||||
### Default labels and annotations
|
||||
|
||||
The action will set the following default labels and annotations based on repository metadata:
|
||||
|
||||
- `org.opencontainers.image.title`
|
||||
- `org.opencontainers.image.description`
|
||||
- `org.opencontainers.image.url`
|
||||
- `org.opencontainers.image.source`
|
||||
- `org.opencontainers.image.version`
|
||||
- `org.opencontainers.image.created`
|
||||
- `org.opencontainers.image.revision`
|
||||
- `org.opencontainers.image.licenses`
|
||||
|
||||
### Customize labels and annotations
|
||||
|
||||
If some [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/master/annotations.md)
|
||||
generated are not suitable as labels/annotations, you can overwrite them like
|
||||
this:
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
labels: |
|
||||
maintainer=CrazyMax
|
||||
org.opencontainers.image.title=MyCustomTitle
|
||||
org.opencontainers.image.description=Another description
|
||||
org.opencontainers.image.vendor=MyCompany
|
||||
```
|
||||
|
||||
Alternatively, you may wish to omit certain labels and annotations from the action output. For example, you may have `LABEL` directives in a Dockerfile that you would prefer to use instead of the labels set by this action. You can omit labels and annotations with `enable=false`.
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
labels: |
|
||||
name=org.opencontainers.image.url,enable=false
|
||||
```
|
||||
|
||||
### Annotation outputs
|
||||
|
||||
Since Buildx 0.12, it is possible to set annotations to your image through the
|
||||
`--annotation` flag.
|
||||
|
||||
With the [`build-push-action`](https://github.com/docker/build-push-action/),
|
||||
you can set the `annotations` input with the value of the `annotations` output
|
||||
of the `metadata-action`:
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
```
|
||||
|
||||
The same can be done with the [`bake-action`](https://github.com/docker/bake-action/):
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.meta.outputs.bake-file-tags }}
|
||||
cwd://${{ steps.meta.outputs.bake-file-annotations }}
|
||||
targets: build
|
||||
```
|
||||
|
||||
### Annotation levels
|
||||
|
||||
Note that annotations can be attached at many different levels within a manifest.
|
||||
By default, the generated annotations will be attached to image manifests,
|
||||
but different registries may expect annotations at different places;
|
||||
a common practice is to read annotations at _image indexes_ if present,
|
||||
which are often used by multi-arch builds to index platform-specific images.
|
||||
If you want to specify level(s) for your annotations, you can use the
|
||||
[`DOCKER_METADATA_ANNOTATIONS_LEVELS` environment variable](#environment-variables)
|
||||
with a comma separated list of all levels the annotations should be attached to (defaults to `manifest`).
|
||||
The following configuration demonstrates the ability to attach annotations to both image manifests and image indexes,
|
||||
though your registry may only need annotations at the index level. (That is, `index` alone may be enough.)
|
||||
Please consult the documentation of your registry.
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
```
|
||||
|
||||
More information about annotations in the [BuildKit documentation](https://github.com/moby/buildkit/blob/master/docs/annotations.md).
|
||||
|
||||
## Notes
|
||||
|
||||
### Image name and tag sanitization
|
||||
|
|
@ -955,98 +1080,6 @@ that you can reuse them further in your workflow using the [`fromJSON` function]
|
|||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
```
|
||||
|
||||
### Overwrite labels and annotations
|
||||
|
||||
If some [OCI Image Format Specification](https://github.com/opencontainers/image-spec/blob/master/annotations.md)
|
||||
generated are not suitable as labels/annotations, you can overwrite them like
|
||||
this:
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
labels: |
|
||||
maintainer=CrazyMax
|
||||
org.opencontainers.image.title=MyCustomTitle
|
||||
org.opencontainers.image.description=Another description
|
||||
org.opencontainers.image.vendor=MyCompany
|
||||
```
|
||||
|
||||
### Annotations
|
||||
|
||||
Since Buildx 0.12, it is possible to set annotations to your image through the
|
||||
`--annotation` flag.
|
||||
|
||||
With the [`build-push-action`](https://github.com/docker/build-push-action/),
|
||||
you can set the `annotations` input with the value of the `annotations` output
|
||||
of the `metadata-action`:
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
```
|
||||
|
||||
The same can be done with the [`bake-action`](https://github.com/docker/bake-action/):
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.meta.outputs.bake-file-tags }}
|
||||
cwd://${{ steps.meta.outputs.bake-file-annotations }}
|
||||
targets: build
|
||||
```
|
||||
|
||||
Note that annotations can be attached at many different levels within a manifest.
|
||||
By default, the generated annotations will be attached to image manifests,
|
||||
but different registries may expect annotations at different places;
|
||||
a common practice is to read annotations at _image indexes_ if present,
|
||||
which are often used by multi-arch builds to index platform-specific images.
|
||||
If you want to specify level(s) for your annotations, you can use the
|
||||
[`DOCKER_METADATA_ANNOTATIONS_LEVELS` environment variable](#environment-variables)
|
||||
with a comma separated list of all levels the annotations should be attached to (defaults to `manifest`).
|
||||
The following configuration demonstrates the ability to attach annotations to both image manifests and image indexes,
|
||||
though your registry may only need annotations at the index level. (That is, `index` alone may be enough.)
|
||||
Please consult the documentation of your registry.
|
||||
|
||||
```yaml
|
||||
-
|
||||
name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: name/app
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
```
|
||||
|
||||
More information about annotations in the [BuildKit documentation](https://github.com/moby/buildkit/blob/master/docs/annotations.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
Want to contribute? Awesome! You can find information about contributing to
|
||||
|
|
|
|||
83
__tests__/annotation.test.ts
Normal file
83
__tests__/annotation.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import {describe, expect, test} from '@jest/globals';
|
||||
|
||||
import {Transform, Annotation} from '../src/annotation';
|
||||
|
||||
describe('annotation transform', () => {
|
||||
test.each([
|
||||
[
|
||||
[`org.opencontainers.image.version=1.1.1`],
|
||||
[
|
||||
{
|
||||
name: `org.opencontainers.image.version`,
|
||||
value: `1.1.1`,
|
||||
enable: true
|
||||
}
|
||||
] as Annotation[],
|
||||
false
|
||||
],
|
||||
[
|
||||
[`name=my.annotation,value="my value",enable=true`],
|
||||
[
|
||||
{
|
||||
name: `my.annotation`,
|
||||
value: `"my value"`,
|
||||
enable: true
|
||||
}
|
||||
] as Annotation[],
|
||||
false
|
||||
],
|
||||
[
|
||||
[`name=my.annotation,value=myvalue,enable=false`],
|
||||
[
|
||||
{
|
||||
name: `my.annotation`,
|
||||
value: `myvalue`,
|
||||
enable: false
|
||||
}
|
||||
] as Annotation[],
|
||||
false
|
||||
],
|
||||
[
|
||||
[`my.annotation=my value`],
|
||||
[
|
||||
{
|
||||
name: `my.annotation`,
|
||||
value: `my value`,
|
||||
enable: true
|
||||
}
|
||||
] as Annotation[],
|
||||
false
|
||||
],
|
||||
[
|
||||
[`name=,value=val`], // empty name
|
||||
undefined,
|
||||
true
|
||||
],
|
||||
[
|
||||
[`name=org.opencontainers.image.url,enable=false`], // empty value
|
||||
[
|
||||
{
|
||||
name: `org.opencontainers.image.url`,
|
||||
value: null,
|
||||
enable: false
|
||||
}
|
||||
] as Annotation[],
|
||||
false
|
||||
],
|
||||
[
|
||||
[`name=my.annotation,value=myvalue,enable=bar`], // invalid enable
|
||||
undefined,
|
||||
true
|
||||
]
|
||||
])('given %p', async (l: string[], expected: Annotation[] | undefined, invalid: boolean) => {
|
||||
try {
|
||||
const annotations = Transform(l);
|
||||
expect(annotations).toEqual(expected);
|
||||
} catch (err) {
|
||||
if (!invalid) {
|
||||
console.error(err);
|
||||
}
|
||||
expect(invalid).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
97
src/annotation.ts
Normal file
97
src/annotation.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import {parse} from 'csv-parse/sync';
|
||||
import * as core from '@actions/core';
|
||||
|
||||
export interface Annotation {
|
||||
name: string;
|
||||
value: string | null;
|
||||
enable: boolean;
|
||||
}
|
||||
|
||||
export function Transform(inputs: string[]): Annotation[] {
|
||||
let annotations: Annotation[] = [];
|
||||
|
||||
for (const input of inputs) {
|
||||
const annotation: Annotation = {name: '', value: null, enable: true};
|
||||
const fields = parse(input, {
|
||||
relaxColumnCount: true,
|
||||
relaxQuotes: true,
|
||||
skipEmptyLines: true
|
||||
})[0];
|
||||
let usesAttributes = false;
|
||||
|
||||
for (const field of fields) {
|
||||
const parts = field
|
||||
.toString()
|
||||
.split('=')
|
||||
.map(item => item.trim());
|
||||
if (parts.length > 0) {
|
||||
const key = parts[0].toLowerCase();
|
||||
if (['name', 'value', 'enable'].includes(key)) {
|
||||
usesAttributes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usesAttributes) {
|
||||
for (const field of fields) {
|
||||
const parts = field
|
||||
.toString()
|
||||
.split('=')
|
||||
.map(item => item.trim());
|
||||
if (parts.length === 1) {
|
||||
annotation.name = parts[0];
|
||||
} else {
|
||||
const key = parts[0].toLowerCase();
|
||||
const value = parts.slice(1).join('='); // preserve '=' in values if any
|
||||
switch (key) {
|
||||
case 'name': {
|
||||
annotation.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
annotation.value = value;
|
||||
break;
|
||||
}
|
||||
case 'enable': {
|
||||
if (!['true', 'false'].includes(value.toLowerCase())) {
|
||||
throw new Error(`Invalid enable attribute value: ${input}`);
|
||||
}
|
||||
annotation.enable = /true/i.test(value);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown annotation attribute: ${input}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const idx = input.indexOf('=');
|
||||
if (idx === -1) {
|
||||
annotation.name = input.trim();
|
||||
} else {
|
||||
annotation.name = input.substring(0, idx).trim();
|
||||
annotation.value = input.substring(idx + 1).trim();
|
||||
}
|
||||
annotation.enable = true;
|
||||
}
|
||||
|
||||
if (annotation.name.length === 0) {
|
||||
throw new Error(`Annotation name attribute empty: ${input}`);
|
||||
}
|
||||
|
||||
annotations.push(annotation);
|
||||
}
|
||||
|
||||
return output(annotations);
|
||||
}
|
||||
|
||||
function output(annotations: Annotation[]): Annotation[] {
|
||||
core.startGroup(`Processing annotations input`);
|
||||
for (const annotation of annotations) {
|
||||
core.info(`name=${annotation.name},value=${annotation.value},enable=${annotation.enable}`);
|
||||
}
|
||||
core.endGroup();
|
||||
return annotations;
|
||||
}
|
||||
19
src/meta.ts
19
src/meta.ts
|
|
@ -12,6 +12,7 @@ import {Inputs, Context} from './context';
|
|||
import * as icl from './image';
|
||||
import * as tcl from './tag';
|
||||
import * as fcl from './flavor';
|
||||
import * as acl from './annotation';
|
||||
|
||||
const defaultShortShaLength = 7;
|
||||
|
||||
|
|
@ -30,6 +31,8 @@ export class Meta {
|
|||
private readonly images: icl.Image[];
|
||||
private readonly tags: tcl.Tag[];
|
||||
private readonly flavor: fcl.Flavor;
|
||||
private readonly annotations: acl.Annotation[];
|
||||
private readonly labels: acl.Annotation[];
|
||||
private readonly date: Date;
|
||||
|
||||
constructor(inputs: Inputs, context: Context, repo: GitHubRepo) {
|
||||
|
|
@ -39,6 +42,8 @@ export class Meta {
|
|||
this.images = icl.Transform(inputs.images);
|
||||
this.tags = tcl.Transform(inputs.tags);
|
||||
this.flavor = fcl.Transform(inputs.flavor);
|
||||
this.annotations = acl.Transform(inputs.annotations);
|
||||
this.labels = acl.Transform(inputs.labels);
|
||||
this.date = new Date();
|
||||
this.version = this.getVersion();
|
||||
}
|
||||
|
|
@ -530,14 +535,14 @@ export class Meta {
|
|||
}
|
||||
|
||||
public getLabels(): Array<string> {
|
||||
return this.getOCIAnnotationsWithCustoms(this.inputs.labels);
|
||||
return this.getOCIAnnotationsWithCustoms(this.labels);
|
||||
}
|
||||
|
||||
public getAnnotations(): Array<string> {
|
||||
return this.getOCIAnnotationsWithCustoms(this.inputs.annotations);
|
||||
return this.getOCIAnnotationsWithCustoms(this.annotations);
|
||||
}
|
||||
|
||||
private getOCIAnnotationsWithCustoms(extra: string[]): Array<string> {
|
||||
private getOCIAnnotationsWithCustoms(annotations: acl.Annotation[]): Array<string> {
|
||||
const res: Array<string> = [
|
||||
`org.opencontainers.image.title=${this.repo.name || ''}`,
|
||||
`org.opencontainers.image.description=${this.repo.description || ''}`,
|
||||
|
|
@ -548,10 +553,12 @@ export class Meta {
|
|||
`org.opencontainers.image.revision=${this.context.sha || ''}`,
|
||||
`org.opencontainers.image.licenses=${this.repo.license?.spdx_id || ''}`
|
||||
];
|
||||
extra.forEach(label => {
|
||||
res.push(this.setGlobalExp(label));
|
||||
annotations.forEach(annotation => {
|
||||
if (annotation.enable && annotation.value != null) {
|
||||
const expandedValue = this.setGlobalExp(annotation.value);
|
||||
res.push(`${annotation.name}=${expandedValue}`);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(
|
||||
new Map<string, string>(
|
||||
res
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue