Skip to content
Navigation Menu
Toggle navigation
Sign in
In this repository
All GitHub Enterprise
↵
Jump to
↵
No suggested jump to results
In this repository
All GitHub Enterprise
↵
Jump to
↵
In this organization
All GitHub Enterprise
↵
Jump to
↵
In this repository
All GitHub Enterprise
↵
Jump to
↵
Sign in
Reseting focus
You signed in with another tab or window.
Reload
to refresh your session.
You signed out in another tab or window.
Reload
to refresh your session.
You switched accounts on another tab or window.
Reload
to refresh your session.
Dismiss alert
{{ message }}
docker
/
metadata-action
Public
Notifications
You must be signed in to change notification settings
Fork
0
Star
0
Code
Pull requests
0
Actions
Security
Insights
Additional navigation options
Code
Pull requests
Actions
Security
Insights
Files
90a1d5c
.github
__tests__
dist
src
context.ts
flavor.ts
github.ts
image.ts
main.ts
meta.ts
pep440.d.ts
tag.ts
test
.dockerignore
.editorconfig
.eslintrc.json
.gitattributes
.gitignore
.prettierrc.json
LICENSE
README.md
UPGRADE.md
action.yml
codecov.yml
dev.Dockerfile
docker-bake.hcl
jest.config.ts
package.json
tsconfig.json
yarn.lock
Breadcrumbs
metadata-action
/
src
/
meta.ts
Blame
Blame
Latest commit
History
History
539 lines (491 loc) · 17.6 KB
Breadcrumbs
metadata-action
/
src
/
meta.ts
Top
File metadata and controls
Code
Blame
539 lines (491 loc) · 17.6 KB
Raw
import * as handlebars from 'handlebars'; import * as fs from 'fs'; import * as path from 'path'; import moment from 'moment-timezone'; import * as pep440 from '@renovate/pep440'; import * as semver from 'semver'; import {Inputs, tmpDir} from './context'; import {ReposGetResponseData} from './github'; import * as icl from './image'; import * as tcl from './tag'; import * as fcl from './flavor'; import * as core from '@actions/core'; import {Context} from '@actions/github/lib/context'; export interface Version { main: string | undefined; partial: string[]; latest: boolean | undefined; } export class Meta { public readonly version: Version; private readonly inputs: Inputs; private readonly context: Context; private readonly repo: ReposGetResponseData; private readonly images: icl.Image[]; private readonly tags: tcl.Tag[]; private readonly flavor: fcl.Flavor; private readonly date: Date; constructor(inputs: Inputs, context: Context, repo: ReposGetResponseData) { // Needs to override Git reference with pr ref instead of upstream branch ref // for pull_request_target event // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target if (/pull_request_target/.test(context.eventName)) { context.ref = `refs/pull/${context.payload.number}/merge`; } // DOCKER_METADATA_PR_HEAD_SHA env var can be used to set associated head // SHA instead of commit SHA that triggered the workflow on pull request // event. if (/true/i.test(process.env.DOCKER_METADATA_PR_HEAD_SHA || '')) { if ((/pull_request/.test(context.eventName) || /pull_request_target/.test(context.eventName)) && context.payload?.pull_request?.head?.sha != undefined) { context.sha = context.payload.pull_request.head.sha; } } this.inputs = inputs; this.context = context; this.repo = repo; this.images = icl.Transform(inputs.images); this.tags = tcl.Transform(inputs.tags); this.flavor = fcl.Transform(inputs.flavor); this.date = new Date(); this.version = this.getVersion(); } private getVersion(): Version { let version: Version = { main: undefined, partial: [], latest: undefined }; for (const tag of this.tags) { const enabled = this.setGlobalExp(tag.attrs['enable']); if (!['true', 'false'].includes(enabled)) { throw new Error(`Invalid value for enable attribute: ${enabled}`); } if (!/true/i.test(enabled)) { continue; } switch (tag.type) { case tcl.Type.Schedule: { version = this.procSchedule(version, tag); break; } case tcl.Type.Semver: { version = this.procSemver(version, tag); break; } case tcl.Type.Pep440: { version = this.procPep440(version, tag); break; } case tcl.Type.Match: { version = this.procMatch(version, tag); break; } case tcl.Type.Ref: { if (tag.attrs['event'] == tcl.RefEvent.Branch) { version = this.procRefBranch(version, tag); } else if (tag.attrs['event'] == tcl.RefEvent.Tag) { version = this.procRefTag(version, tag); } else if (tag.attrs['event'] == tcl.RefEvent.PR) { version = this.procRefPr(version, tag); } break; } case tcl.Type.Edge: { version = this.procEdge(version, tag); break; } case tcl.Type.Raw: { version = this.procRaw(version, tag); break; } case tcl.Type.Sha: { version = this.procSha(version, tag); break; } } } version.partial = version.partial.filter((item, index) => version.partial.indexOf(item) === index); if (version.latest == undefined) { version.latest = false; } return version; } private procSchedule(version: Version, tag: tcl.Tag): Version { if (!/schedule/.test(this.context.eventName)) { return version; } const currentDate = this.date; const vraw = this.setValue( handlebars.compile(tag.attrs['pattern'])({ date: function (format, options) { const m = moment(currentDate); let tz = 'UTC'; Object.keys(options.hash).forEach(key => { switch (key) { case 'tz': tz = options.hash[key]; break; default: throw new Error(`Unknown ${key} attribute`); } }); return m.tz(tz).format(format); } }), tag ); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private procSemver(version: Version, tag: tcl.Tag): Version { if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { return version; } let vraw: string; if (tag.attrs['value'].length > 0) { vraw = this.setGlobalExp(tag.attrs['value']); } else { vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); } if (!semver.valid(vraw)) { core.warning(`${vraw} is not a valid semver. More info: https://semver.org/`); return version; } let latest = false; const sver = semver.parse(vraw, { includePrerelease: true }); if (semver.prerelease(vraw)) { if (Meta.isRawStatement(tag.attrs['pattern'])) { vraw = this.setValue(handlebars.compile(tag.attrs['pattern'])(sver), tag); } else { vraw = this.setValue(handlebars.compile('{{version}}')(sver), tag); } } else { vraw = this.setValue(handlebars.compile(tag.attrs['pattern'])(sver), tag); latest = true; } return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'); } private procPep440(version: Version, tag: tcl.Tag): Version { if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { return version; } let vraw: string; if (tag.attrs['value'].length > 0) { vraw = this.setGlobalExp(tag.attrs['value']); } else { vraw = this.context.ref.replace(/^refs\/tags\//g, '').replace(/\//g, '-'); } if (!pep440.valid(vraw)) { core.warning(`${vraw} does not conform to PEP 440. More info: https://www.python.org/dev/peps/pep-0440`); return version; } let latest = false; const pver = pep440.explain(vraw); if (pver.is_prerelease || pver.is_postrelease || pver.is_devrelease) { if (Meta.isRawStatement(tag.attrs['pattern'])) { vraw = this.setValue(vraw, tag); } else { vraw = this.setValue(pep440.clean(vraw), tag); } } else { vraw = this.setValue( handlebars.compile(tag.attrs['pattern'])({ raw: function () { return vraw; }, version: function () { return pep440.clean(vraw); }, major: function () { return pep440.major(vraw); }, minor: function () { return pep440.minor(vraw); }, patch: function () { return pep440.patch(vraw); } }), tag ); latest = true; } return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? latest : this.flavor.latest == 'true'); } private procMatch(version: Version, tag: tcl.Tag): Version { if (!/^refs\/tags\//.test(this.context.ref) && tag.attrs['value'].length == 0) { return version; } let vraw: string; if (tag.attrs['value'].length > 0) { vraw = this.setGlobalExp(tag.attrs['value']); } else { vraw = this.context.ref.replace(/^refs\/tags\//g, ''); } let tmatch; const isRegEx = tag.attrs['pattern'].match(/^\/(.+)\/(.*)$/); if (isRegEx) { tmatch = vraw.match(new RegExp(isRegEx[1], isRegEx[2])); } else { tmatch = vraw.match(tag.attrs['pattern']); } if (!tmatch) { core.warning(`${tag.attrs['pattern']} does not match ${vraw}.`); return version; } if (typeof tmatch[tag.attrs['group']] === 'undefined') { core.warning(`Group ${tag.attrs['group']} does not exist for ${tag.attrs['pattern']} pattern.`); return version; } vraw = this.setValue(tmatch[tag.attrs['group']], tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? true : this.flavor.latest == 'true'); } private procRefBranch(version: Version, tag: tcl.Tag): Version { if (!/^refs\/heads\//.test(this.context.ref)) { return version; } const vraw = this.setValue(this.context.ref.replace(/^refs\/heads\//g, ''), tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private procRefTag(version: Version, tag: tcl.Tag): Version { if (!/^refs\/tags\//.test(this.context.ref)) { return version; } const vraw = this.setValue(this.context.ref.replace(/^refs\/tags\//g, ''), tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? true : this.flavor.latest == 'true'); } private procRefPr(version: Version, tag: tcl.Tag): Version { if (!/^refs\/pull\//.test(this.context.ref)) { return version; } const vraw = this.setValue(this.context.ref.replace(/^refs\/pull\//g, '').replace(/\/merge$/g, ''), tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private procEdge(version: Version, tag: tcl.Tag): Version { if (!/^refs\/heads\//.test(this.context.ref)) { return version; } const val = this.context.ref.replace(/^refs\/heads\//g, ''); if (tag.attrs['branch'].length == 0) { tag.attrs['branch'] = this.repo.default_branch; } if (tag.attrs['branch'] != val) { return version; } const vraw = this.setValue('edge', tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private procRaw(version: Version, tag: tcl.Tag): Version { const vraw = this.setValue(this.setGlobalExp(tag.attrs['value']), tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private procSha(version: Version, tag: tcl.Tag): Version { if (!this.context.sha) { return version; } let val = this.context.sha; if (tag.attrs['format'] === tcl.ShaFormat.Short) { val = this.context.sha.substring(0, 7); } const vraw = this.setValue(val, tag); return Meta.setVersion(version, vraw, this.flavor.latest == 'auto' ? false : this.flavor.latest == 'true'); } private static setVersion(version: Version, val: string, latest: boolean): Version { if (val.length == 0) { return version; } val = Meta.sanitizeTag(val); if (version.main == undefined) { version.main = val; } else if (val !== version.main) { version.partial.push(val); } if (version.latest == undefined) { version.latest = latest; } return version; } public static isRawStatement(pattern: string): boolean { try { const hp = handlebars.parseWithoutProcessing(pattern); if (hp.body.length == 1 && hp.body[0].type == 'MustacheStatement') { return hp.body[0]['path']['parts'].length == 1 && hp.body[0]['path']['parts'][0] == 'raw'; } } catch (err) { return false; } return false; } private setValue(val: string, tag: tcl.Tag): string { if (Object.prototype.hasOwnProperty.call(tag.attrs, 'prefix')) { val = `${this.setGlobalExp(tag.attrs['prefix'])}${val}`; } else if (this.flavor.prefix.length > 0) { val = `${this.setGlobalExp(this.flavor.prefix)}${val}`; } if (Object.prototype.hasOwnProperty.call(tag.attrs, 'suffix')) { val = `${val}${this.setGlobalExp(tag.attrs['suffix'])}`; } else if (this.flavor.suffix.length > 0) { val = `${val}${this.setGlobalExp(this.flavor.suffix)}`; } return val; } private setGlobalExp(val): string { const ctx = this.context; const currentDate = this.date; return handlebars.compile(val)({ branch: function () { if (!/^refs\/heads\//.test(ctx.ref)) { return ''; } return ctx.ref.replace(/^refs\/heads\//g, ''); }, tag: function () { if (!/^refs\/tags\//.test(ctx.ref)) { return ''; } return ctx.ref.replace(/^refs\/tags\//g, ''); }, sha: function () { return ctx.sha.substring(0, 7); }, base_ref: function () { if (/^refs\/tags\//.test(ctx.ref) && ctx.payload?.base_ref != undefined) { return ctx.payload.base_ref.replace(/^refs\/heads\//g, ''); } // FIXME: keep this for backward compatibility even if doesn't always seem // to return the expected branch. See the comment below. if (/^refs\/pull\//.test(ctx.ref) && ctx.payload?.pull_request?.base?.ref != undefined) { return ctx.payload.pull_request.base.ref; } return ''; }, is_default_branch: function () { const branch = ctx.ref.replace(/^refs\/heads\//g, ''); // TODO: "base_ref" is available in the push payload but doesn't always seem to // return the expected branch when the push tag event occurs. It's also not // documented in GitHub docs: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#push // more context: https://github.com/docker/metadata-action/pull/192#discussion_r854673012 // if (/^refs\/tags\//.test(ctx.ref) && ctx.payload?.base_ref != undefined) { // branch = ctx.payload.base_ref.replace(/^refs\/heads\//g, ''); // } if (branch == undefined || branch.length == 0) { return 'false'; } if (ctx.payload?.repository?.default_branch == branch) { return 'true'; } // following events always trigger for last commit on default branch // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows if (/create/.test(ctx.eventName) || /discussion/.test(ctx.eventName) || /issues/.test(ctx.eventName) || /schedule/.test(ctx.eventName)) { return 'true'; } return 'false'; }, date: function (format, options) { const m = moment(currentDate); let tz = 'UTC'; Object.keys(options.hash).forEach(key => { switch (key) { case 'tz': tz = options.hash[key]; break; default: throw new Error(`Unknown ${key} attribute`); } }); return m.tz(tz).format(format); } }); } private getImageNames(): Array<string> { const images: Array<string> = []; for (const image of this.images) { if (!image.enable) { continue; } images.push(Meta.sanitizeImageName(image.name)); } return images; } public getTags(): Array<string> { if (!this.version.main) { return []; } const tags: Array<string> = []; for (const imageName of this.getImageNames()) { tags.push(`${imageName}:${this.version.main}`); for (const partial of this.version.partial) { tags.push(`${imageName}:${partial}`); } if (this.version.latest) { const latestTag = `${this.flavor.prefixLatest ? this.flavor.prefix : ''}latest${this.flavor.suffixLatest ? this.flavor.suffix : ''}`; tags.push(`${imageName}:${Meta.sanitizeTag(latestTag)}`); } } return tags; } public getLabels(): Array<string> { const labels: Array<string> = [ `org.opencontainers.image.title=${this.repo.name || ''}`, `org.opencontainers.image.description=${this.repo.description || ''}`, `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.revision=${this.context.sha || ''}`, `org.opencontainers.image.licenses=${this.repo.license?.spdx_id || ''}` ]; labels.push(...this.inputs.labels); return labels; } public getJSON(): unknown { return { tags: this.getTags(), labels: this.getLabels().reduce((res, label) => { const matches = label.match(/([^=]*)=(.*)/); if (!matches) { return res; } res[matches[1]] = matches[2]; return res; }, {}) }; } public getBakeFile(): string { const bakeFile = path.join(tmpDir(), 'docker-metadata-action-bake.json').split(path.sep).join(path.posix.sep); fs.writeFileSync( bakeFile, JSON.stringify( { target: { [this.inputs.bakeTarget]: { tags: this.getTags(), labels: this.getLabels().reduce((res, label) => { const matches = label.match(/([^=]*)=(.*)/); if (!matches) { return res; } res[matches[1]] = matches[2]; return res; }, {}), args: { DOCKER_META_IMAGES: this.getImageNames().join(','), DOCKER_META_VERSION: this.version.main } } } }, null, 2 ) ); return bakeFile; } private static sanitizeImageName(name: string): string { return name.toLowerCase(); } private static sanitizeTag(tag: string): string { return tag.replace(/[^a-zA-Z0-9._-]+/g, '-'); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
You can’t perform that action at this time.