Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allows to specify context to fetch git data #248

Merged
merged 1 commit into from Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 25 additions & 9 deletions README.md
Expand Up @@ -22,6 +22,7 @@ ___
* [inputs](#inputs)
* [outputs](#outputs)
* [environment variables](#environment-variables)
* [`context` input](#context-input)
* [`images` input](#images-input)
* [`flavor` input](#flavor-input)
* [`tags` input](#tags-input)
Expand Down Expand Up @@ -275,15 +276,16 @@ Following inputs can be used as `step.with` keys
> org.opencontainers.image.vendor=MyCompany
> ```

| Name | Type | Description |
|---------------------|--------|----------------------------------------------------------|
| `images` | List | List of Docker images to use as base name for tags |
| `tags` | List | List of [tags](#tags-input) as key-value pair attributes |
| `flavor` | List | [Flavor](#flavor-input) to apply |
| `labels` | List | List of custom labels |
| `sep-tags` | String | Separator to use for tags output (default `\n`) |
| `sep-labels` | String | Separator to use for labels output (default `\n`) |
| `bake-target` | String | Bake target name (default `docker-metadata-action`) |
| Name | Type | Description |
|---------------------|--------|-------------------------------------------------------------------------------|
| `context` | String | Where to get context data. Allowed options are: `workflow` (default), `git`. |
| `images` | List | List of Docker images to use as base name for tags |
| `tags` | List | List of [tags](#tags-input) as key-value pair attributes |
| `flavor` | List | [Flavor](#flavor-input) to apply |
| `labels` | List | List of custom labels |
| `sep-tags` | String | Separator to use for tags output (default `\n`) |
| `sep-labels` | String | Separator to use for labels output (default `\n`) |
| `bake-target` | String | Bake target name (default `docker-metadata-action`) |

### outputs

Expand Down Expand Up @@ -320,6 +322,20 @@ So it can be used with our [Docker Build Push action](https://github.com/docker/
|-------------------------------|------|------------------------------------------------------------------------------------------------------------|
| `DOCKER_METADATA_PR_HEAD_SHA` | Bool | If `true`, set associated head SHA instead of commit SHA that triggered the workflow on pull request event |

## `context` input

`context` defines where to get context metadata:

```yaml
# default
context: workflow
# or
context: git
```

* `workflow`: Get context metadata from the workflow (GitHub context). See https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
* `git`: Get context metadata from the workflow and overrides some of them with current Git context, such as `ref` and `sha`.

## `images` input

`images` defines a list of Docker images to use as base name for [`tags`](#tags-input):
Expand Down
54 changes: 47 additions & 7 deletions __tests__/context.test.ts
@@ -1,6 +1,9 @@
import {beforeEach, describe, expect, test} from '@jest/globals';

import * as context from '../src/context';
import {Context} from '@actions/github/lib/context';
import {beforeEach, describe, expect, test, it, jest} from '@jest/globals';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
import {ContextSource, getInputs, Inputs} from '../src/context';

describe('getInputs', () => {
beforeEach(() => {
Expand All @@ -20,6 +23,7 @@ describe('getInputs', () => {
['images', 'moby/buildkit\nghcr.io/moby/mbuildkit'],
]),
{
context: ContextSource.workflow,
bakeTarget: 'docker-metadata-action',
flavor: [],
githubToken: '',
Expand All @@ -28,7 +32,7 @@ describe('getInputs', () => {
sepLabels: '\n',
sepTags: '\n',
tags: [],
} as context.Inputs
} as Inputs
],
[
1,
Expand All @@ -39,6 +43,7 @@ describe('getInputs', () => {
['sep-tags', ','],
]),
{
context: ContextSource.workflow,
bakeTarget: 'metadata',
flavor: [],
githubToken: '',
Expand All @@ -47,19 +52,54 @@ describe('getInputs', () => {
sepLabels: ',',
sepTags: ',',
tags: [],
} as context.Inputs
} as Inputs
]
])(
'[%d] given %p as inputs, returns %p',
async (num: number, inputs: Map<string, string>, expected: context.Inputs) => {
async (num: number, inputs: Map<string, string>, expected: Inputs) => {
inputs.forEach((value: string, name: string) => {
setInput(name, value);
});
expect(await context.getInputs()).toEqual(expected);
expect(await getInputs()).toEqual(expected);
}
);
});

describe('getContext', () => {
it('get context with workflow', async () => {
process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures/event_create_branch.env')));

jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {getContext} = require('../src/context');
const contextResult = await getContext(ContextSource.workflow);

expect(contextResult.ref).toEqual('refs/heads/dev');
expect(contextResult.sha).toEqual('5f3331d7f7044c18ca9f12c77d961c4d7cf3276a');
});

it('get context with git', async () => {
jest.resetModules();

// eslint-disable-next-line @typescript-eslint/no-var-requires
const git = require('@docker/actions-toolkit/lib/git');
jest.spyOn(git.Git, 'context').mockImplementation((): Promise<Context> => {
return Promise.resolve({
ref: 'refs/heads/git-test',
sha: 'git-test-sha'
} as Context);
});

// eslint-disable-next-line @typescript-eslint/no-var-requires
const {getContext} = require('../src/context');

const contextResult = await getContext(ContextSource.git);

expect(contextResult.ref).toEqual('refs/heads/git-test');
expect(contextResult.sha).toEqual('git-test-sha');
});
});

// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
function getInputName(name: string): string {
return `INPUT_${name.replace(/ /g, '_').toUpperCase()}`;
Expand Down
33 changes: 24 additions & 9 deletions __tests__/meta.test.ts
Expand Up @@ -2,12 +2,10 @@ import {beforeEach, describe, expect, jest, test} from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as dotenv from 'dotenv';
import {Context} from '@actions/github/lib/context';
import {GitHub} from '@docker/actions-toolkit/lib/github';
import {Toolkit} from '@docker/actions-toolkit/lib/toolkit';
import {GitHubRepo} from '@docker/actions-toolkit/lib/types/github';

import {getInputs, Inputs} from '../src/context';
import {ContextSource, getInputs, Inputs} from '../src/context';
import {Meta, Version} from '../src/meta';

import repoFixture from './fixtures/repo.json';
Expand All @@ -23,6 +21,17 @@ jest.mock('moment-timezone', () => {
return () => (jest.requireActual('moment-timezone') as typeof import('moment-timezone'))('2020-01-10T00:30:00.000Z');
});

/**
* Get a workflow context based on the current environment variables.
*/
const getFreshWorkflowContext = async () => {
jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {getContext} = require('../src/context');
const context = await getContext(ContextSource.workflow);
return context;
};

beforeEach(() => {
jest.clearAllMocks();
Object.keys(process.env).forEach(function (key) {
Expand All @@ -49,7 +58,8 @@ const tagsLabelsTest = async (name: string, envFile: string, inputs: Inputs, exV
process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures', envFile)));
const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta({...getInputs(), ...inputs}, new Context(), repo);
const context = await getFreshWorkflowContext();
const meta = new Meta({...getInputs(), ...inputs}, context, repo);
crazy-max marked this conversation as resolved.
Show resolved Hide resolved

const version = meta.version;
expect(version).toEqual(exVersion);
Expand Down Expand Up @@ -2766,7 +2776,8 @@ describe('pr-head-sha', () => {

const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta({...getInputs(), ...inputs}, new Context(), repo);
const context = await getFreshWorkflowContext();
const meta = new Meta({...getInputs(), ...inputs}, context, repo);

const version = meta.version;
expect(version).toEqual(exVersion);
Expand Down Expand Up @@ -3708,7 +3719,8 @@ describe('json', () => {

const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta({...getInputs(), ...inputs}, new Context(), repo);
const context = await getFreshWorkflowContext();
const meta = new Meta({...getInputs(), ...inputs}, context, repo);

const jsonOutput = meta.getJSON();
expect(jsonOutput).toEqual(exJSON);
Expand Down Expand Up @@ -4014,7 +4026,8 @@ describe('bake', () => {

const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta({...getInputs(), ...inputs}, new Context(), repo);
const context = await getFreshWorkflowContext();
const meta = new Meta({...getInputs(), ...inputs}, context, repo);

const bakeFile = meta.getBakeFile();
expect(JSON.parse(fs.readFileSync(bakeFile, 'utf8'))).toEqual(exBakeDefinition);
Expand Down Expand Up @@ -4056,11 +4069,13 @@ describe('sepTags', () => {
"user/app:dev,user/app:my,user/app:custom,user/app:tags"
]
])('given %p with %p event', async (name: string, envFile: string, inputs: Inputs, expTags: string) => {

process.env = dotenv.parse(fs.readFileSync(path.join(__dirname, 'fixtures', envFile)));

const toolkit = new Toolkit();
const repo = await toolkit.github.repoData();
const meta = new Meta({...getInputs(), ...inputs}, new Context(), repo);
const context = await getFreshWorkflowContext();
const meta = new Meta({...getInputs(), ...inputs}, context, repo);

expect(meta.getTags().join(inputs.sepTags)).toEqual(expTags);
});
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Expand Up @@ -7,6 +7,10 @@ branding:
color: 'blue'

inputs:
context:
description: 'Where to get context data. Allowed options are "workflow" (default), "git".'
default: "workflow"
required: true
images:
description: 'List of Docker images to use as base name for tags'
required: true
Expand Down
22 changes: 11 additions & 11 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions src/context.ts
@@ -1,7 +1,11 @@
import * as core from '@actions/core';
import {Context} from '@actions/github/lib/context';
import {Util} from '@docker/actions-toolkit/lib/util';
import {Git} from '@docker/actions-toolkit/lib/git';
import {GitHub} from '@docker/actions-toolkit/lib/github';

export interface Inputs {
context: ContextSource;
images: string[];
tags: string[];
flavor: string[];
Expand All @@ -14,6 +18,7 @@ export interface Inputs {

export function getInputs(): Inputs {
return {
context: (core.getInput('context') || ContextSource.workflow) as ContextSource,
images: Util.getInputList('images', {ignoreComma: true}),
tags: Util.getInputList('tags', {ignoreComma: true}),
flavor: Util.getInputList('flavor', {ignoreComma: true}),
Expand All @@ -24,3 +29,45 @@ export function getInputs(): Inputs {
githubToken: core.getInput('github-token')
};
}

export enum ContextSource {
workflow = 'workflow',
git = 'git'
}

export async function getContext(source: ContextSource): Promise<Context> {
switch (source) {
case ContextSource.workflow:
return getContextFromWorkflow();
case ContextSource.git:
return await getContextFromGit();
default:
throw new Error(`Invalid context source: ${source}`);
}
}

function getContextFromWorkflow(): Context {
const context = GitHub.context;

// 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;
}
}

return context;
}

async function getContextFromGit(): Promise<Context> {
return await Git.context();
}
8 changes: 3 additions & 5 deletions src/main.ts
@@ -1,11 +1,9 @@
import * as fs from 'fs';
import * as core from '@actions/core';
import * as actionsToolkit from '@docker/actions-toolkit';
import {Context} from '@actions/github/lib/context';
import {GitHub} from '@docker/actions-toolkit/lib/github';
import {Toolkit} from '@docker/actions-toolkit/lib/toolkit';

import {getInputs, Inputs} from './context';
import {getContext, getInputs, Inputs} from './context';
import {Meta, Version} from './meta';

function setOutput(name: string, value: string) {
Expand All @@ -16,13 +14,13 @@ function setOutput(name: string, value: string) {
actionsToolkit.run(
// main
async () => {
const inputs: Inputs = await getInputs();
const inputs: Inputs = getInputs();
if (inputs.images.length == 0) {
throw new Error(`images input required`);
}

const toolkit = new Toolkit({githubToken: inputs.githubToken});
const context: Context = GitHub.context;
const context = await getContext(inputs.context);
const repo = await toolkit.github.repoData();

await core.group(`Context info`, async () => {
Expand Down