Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,14 @@ describe("DockerHub Queries", () => {
dockerFile: "Dockerfile.alpine",
phpTag: "8.3.8-fpm-alpine",
phpExtTag: "8.3.8-fpm-alpine-ext",
phpExtMinorTag: "8.3-fpm-alpine-ext",
phpExtMajorTag: "8-fpm-alpine-ext",
extList: mockExtList,
moveMinor: true,
moveMajor: true,
latest: false,
}));
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("PHPTag doesn't exists, do not check PHPExt repo", async () => {
Expand All @@ -77,6 +82,7 @@ describe("DockerHub Queries", () => {

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("PHPTag exists, PHPExtTag doesn't exists", async () => {
Expand All @@ -90,9 +96,14 @@ describe("DockerHub Queries", () => {
dockerFile: "Dockerfile.alpine",
phpTag: "8.3.8-fpm-alpine",
phpExtTag: "8.3.8-fpm-alpine-ext",
phpExtMinorTag: "8.3-fpm-alpine-ext",
phpExtMajorTag: "8-fpm-alpine-ext",
extList: mockExtList,
moveMinor: true,
moveMajor: true,
latest: true,
}));
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("PHPTag doesn't exists, PHPExtTag doesn't exists", async () => {
Expand All @@ -102,16 +113,41 @@ describe("DockerHub Queries", () => {

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("PHPTag exists, PHPExtTag exists", async () => {
test("PHPTag exists, PHPExtTag exists, aliases missing", async () => {
dhtInitMock = jest.spyOn(DockerHubTags, "init").mockImplementation((namespace) => {
const tags = namespace === OFFICIALIMAGES_NAMESPACE ? phpTagInfo : phpExtTagInfo;
return Promise.resolve(new (DockerHubTags as unknown as new (tags: unknown[]) => DockerHubTags)(tags));
});

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", contextJson({
phpExtTag: "8.3.8-fpm-alpine-ext",
phpExtMinorTag: "8.3-fpm-alpine-ext",
phpExtMajorTag: "8-fpm-alpine-ext",
retagMinor: true,
retagMajor: true,
}));
});

test("PHPTag exists, PHPExtTag exists, aliases already present", async () => {
const phpExtWithAliases = [
...phpExtTagInfo,
{...phpExtTagInfo[1], name: "8.3-fpm-alpine-ext", id: 1},
{...phpExtTagInfo[1], name: "8-fpm-alpine-ext", id: 2},
];

dhtInitMock = jest.spyOn(DockerHubTags, "init").mockImplementation((namespace) => {
const tags = namespace === OFFICIALIMAGES_NAMESPACE ? phpTagInfo : phpExtWithAliases;
return Promise.resolve(new (DockerHubTags as unknown as new (tags: unknown[]) => DockerHubTags)(tags));
});

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("PHPTag doesn't, PHPExtTag exists", async () => {
Expand All @@ -122,6 +158,7 @@ describe("DockerHub Queries", () => {

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("Push PHPExtTag with an old tag", async () => {
Expand All @@ -142,9 +179,37 @@ describe("DockerHub Queries", () => {
dockerFile: "Dockerfile.alpine",
phpTag: "8.3.3-fpm-alpine",
phpExtTag: "8.3.3-fpm-alpine-ext",
phpExtMinorTag: "8.3-fpm-alpine-ext",
phpExtMajorTag: "8-fpm-alpine-ext",
extList: mockExtList,
moveMinor: false,
moveMajor: false,
latest: false,
}));
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("Old PHPExtTag already exists, not head of line", async () => {
mockGetInputWith({
php_version: "8.3.3",
php_ext_namespace: "besogon1",
php_type: "fpm",
php_ext_suffix: "ext",
});

const phpExtWithOldPatch = [
...phpExtTagInfo,
{...phpExtTagInfo[1], name: "8.3.3-fpm-alpine-ext", id: 3},
];

dhtInitMock = jest.spyOn(DockerHubTags, "init").mockImplementation((namespace) => {
const tags = namespace === OFFICIALIMAGES_NAMESPACE ? phpTagInfo : phpExtWithOldPatch;
return Promise.resolve(new (DockerHubTags as unknown as new (tags: unknown[]) => DockerHubTags)(tags));
});

await run();
expect(mockSetOutput).toHaveBeenCalledWith("context", "[]");
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});

test("Push PHPExtTag with an New tag", async () => {
Expand All @@ -165,9 +230,14 @@ describe("DockerHub Queries", () => {
dockerFile: "Dockerfile.alpine",
phpTag: "8.3.9-fpm-alpine",
phpExtTag: "8.3.9-fpm-alpine-ext",
phpExtMinorTag: "8.3-fpm-alpine-ext",
phpExtMajorTag: "8-fpm-alpine-ext",
extList: mockExtList,
moveMinor: true,
moveMajor: true,
latest: true,
}));
expect(mockSetOutput).toHaveBeenCalledWith("retag_context", "[]");
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, test, expect } from '@jest/globals';
import {getOsNameFromDockerFile, getPHPTag, getPHPExtTag} from "@src/tools"
import {
getOsNameFromDockerFile,
getPHPTag,
getPHPExtTag,
getVersionAliases,
getPHPExtMinorTag,
getPHPExtMajorTag,
} from "@src/tools"

describe("Test utils", () => {
test("getOsNameFromDockerFile: remove docker prefix", () => {
Expand All @@ -14,4 +21,16 @@ describe("Test utils", () => {
test("getPHPTag: check name", () => {
expect(getPHPExtTag("8.3.8", "alpine", "ext", "fpm")).toBe("8.3.8-fpm-alpine-ext");
});

test("getVersionAliases: patch version", () => {
expect(getVersionAliases("8.3.8")).toEqual({major: "8", minor: "8.3"});
});

test("getPHPExtMinorTag: check name", () => {
expect(getPHPExtMinorTag("8.3.8", "alpine", "ext", "fpm")).toBe("8.3-fpm-alpine-ext");
});

test("getPHPExtMajorTag: check name", () => {
expect(getPHPExtMajorTag("8.3.8", "alpine", "ext", "fpm")).toBe("8-fpm-alpine-ext");
});
});
4 changes: 3 additions & 1 deletion .github/actions/extract-image-context-action/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ inputs:

outputs:
context:
description: 'Image context'
description: 'Image context for build and push'
retag_context:
description: 'Image context for retagging missing floating aliases on Docker Hub'

runs:
using: 'node24'
Expand Down
54 changes: 27 additions & 27 deletions .github/actions/extract-image-context-action/dist/index.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": "tsc && esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/index.js --minify --legal-comments=none",
"test": "jest"
},
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
"packageManager": "pnpm@11.25.0+sha512.5cde925b4f075f725eb71fbae18a42ffe784524789f19b61c731cb8721ec28aaee160e01a8d5af4fedb2a42cdbf300efe23db356b0d4a17b4d63e11f8ab7c956",
"devDependencies": {
"@jest/globals": "^30.4.1",
"@types/jest": "^30.0.0",
Expand Down
104 changes: 83 additions & 21 deletions .github/actions/extract-image-context-action/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
getOsNameFromDockerFile,
getPHPTag,
getPHPExtTag,
getPHPExtMinorTag,
getPHPExtMajorTag,
isError,
getPHPTags,
getPHPExtTags
} from "./tools"
import {loadExtList} from "./php-extensions";
import {ImageContext} from "./types";
import {ImageContext, RetagContext} from "./types";

export async function run() {
try {
Expand All @@ -29,15 +31,20 @@ export async function run() {
checkPhpextTag ? getPHPExtTags(phpExtNamespace) : undefined,
].filter((v): v is Promise<DockerHubTags> => typeof v !== "undefined"));

contextes = filterContextes(contextes, dhtPHPTags, dhtPHPExtTags);
contextes = filterOfficialPhpTags(contextes, dhtPHPTags);
markFloatingTags(contextes, dhtPHPExtTags);

const possibleLatestContext = contextes.filter(context => context.dockerFile.endsWith("alpine")).at(0);
if (typeof possibleLatestContext !== "undefined") {
markLatestIfRequired(possibleLatestContext, dhtPHPExtTags);
}

core.debug(`Contextes: ${JSON.stringify(contextes)}`);
core.setOutput("context", JSON.stringify(contextes));
const {buildContextes, retagContextes} = splitContextes(contextes, dhtPHPExtTags);

core.debug(`Contextes: ${JSON.stringify(buildContextes)}`);
core.debug(`Retag contextes: ${JSON.stringify(retagContextes)}`);
core.setOutput("context", JSON.stringify(buildContextes));
core.setOutput("retag_context", JSON.stringify(retagContextes));
} catch (error: unknown) {
if (isError(error)) {
core.setFailed(error.message);
Expand All @@ -46,29 +53,84 @@ export async function run() {
}

function initContextes(fileNames: string[], phpVersion: string, suffix: string, phpType: string, extList: string): ImageContext[] {
return fileNames.map(fileName => ({
dockerFile: fileName,
phpTag: getPHPTag(phpVersion, getOsNameFromDockerFile(fileName), phpType),
phpExtTag: getPHPExtTag(phpVersion, getOsNameFromDockerFile(fileName), suffix, phpType),
extList,
latest: false
}))
return fileNames.map(fileName => {
const osName = getOsNameFromDockerFile(fileName);
return {
dockerFile: fileName,
phpTag: getPHPTag(phpVersion, osName, phpType),
phpExtTag: getPHPExtTag(phpVersion, osName, suffix, phpType),
phpExtMinorTag: getPHPExtMinorTag(phpVersion, osName, suffix, phpType),
phpExtMajorTag: getPHPExtMajorTag(phpVersion, osName, suffix, phpType),
extList,
moveMinor: false,
moveMajor: false,
latest: false
};
})
}

function filterOfficialPhpTags(contextes: ImageContext[], dhtPHPTags: DockerHubTags) {
const phpTags = dhtPHPTags.getAllTags()
.filter(tag => contextes.some(c => c.phpTag === tag.name))
.map(tag => tag.name);

return contextes.filter(({phpTag}) => phpTags.includes(phpTag));
}

function filterContextes(contextes: ImageContext[], dhtPHPTags: DockerHubTags, dhtPHPExtTags?: DockerHubTags) {
const checkPhpextTag = typeof dhtPHPExtTags !== "undefined";
function splitContextes(contextes: ImageContext[], dhtPHPExtTags?: DockerHubTags): {
buildContextes: ImageContext[],
retagContextes: RetagContext[],
} {
if (typeof dhtPHPExtTags === "undefined") {
return {buildContextes: contextes, retagContextes: []};
}

const existingPhpExtTags = new Set(dhtPHPExtTags.getAllTags().map(tag => tag.name));
const buildContextes: ImageContext[] = [];
const retagContextes: RetagContext[] = [];

for (const context of contextes) {
if (!existingPhpExtTags.has(context.phpExtTag)) {
buildContextes.push(context);
continue;
}

let phpTags = contextes.map(c => c.phpTag);
let phpExtTags = checkPhpextTag ? contextes.map(c=> c.phpExtTag) : [];
const retagMinor = context.moveMinor
&& typeof dhtPHPExtTags.getTag(context.phpExtMinorTag) === "undefined";
const retagMajor = context.moveMajor
&& typeof dhtPHPExtTags.getTag(context.phpExtMajorTag) === "undefined";

phpTags = dhtPHPTags.getAllTags().filter(tag => phpTags.includes(tag.name)).map(tag => tag.name);
if (checkPhpextTag) {
phpExtTags = dhtPHPExtTags.getAllTags().filter(tag => phpExtTags.includes(tag.name)).map(tag => tag.name);
if (retagMinor || retagMajor) {
retagContextes.push({
phpExtTag: context.phpExtTag,
phpExtMinorTag: context.phpExtMinorTag,
phpExtMajorTag: context.phpExtMajorTag,
retagMinor,
retagMajor,
});
}
}

return contextes.filter(({phpTag, phpExtTag}) => {
return phpTags.includes(phpTag) && (checkPhpextTag ? !phpExtTags.includes(phpExtTag) : true)
});
return {buildContextes, retagContextes};
}

function markFloatingTags(contextes: ImageContext[], dhtPHPExtTags?: DockerHubTags) {
for (const context of contextes) {
const canMoveMinor = context.phpExtMinorTag !== context.phpExtTag;
const canMoveMajor = context.phpExtMajorTag !== context.phpExtTag
&& context.phpExtMajorTag !== context.phpExtMinorTag;

if (typeof dhtPHPExtTags === "undefined") {
context.moveMinor = canMoveMinor;
context.moveMajor = canMoveMajor;
continue;
}

context.moveMinor = canMoveMinor
&& typeof dhtPHPExtTags.getRecent(`~${context.phpExtTag}`) === "undefined";
context.moveMajor = canMoveMajor
&& typeof dhtPHPExtTags.getRecent(`^${context.phpExtTag}`) === "undefined";
}
}

function markLatestIfRequired(context:ImageContext, dhtPHPExtTags?:DockerHubTags) {
Expand Down
18 changes: 18 additions & 0 deletions .github/actions/extract-image-context-action/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ export function getPHPExtTag(phpVersion:string, osName:string, extSuffix: string
return `${officialTag}-${extSuffix}`;
}

/** Split semver-ish PHP version into major / major.minor aliases (e.g. 8.2.33 → { major: "8", minor: "8.2" }). */
export function getVersionAliases(phpVersion: string): {major: string; minor: string} {
const parts = phpVersion.split(".");
const major = parts[0] ?? phpVersion;
const minor = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : major;
return {major, minor};
}

export function getPHPExtMinorTag(phpVersion: string, osName: string, extSuffix: string, phpType?: string) {
const {minor} = getVersionAliases(phpVersion);
return getPHPExtTag(minor, osName, extSuffix, phpType);
}

export function getPHPExtMajorTag(phpVersion: string, osName: string, extSuffix: string, phpType?: string) {
const {major} = getVersionAliases(phpVersion);
return getPHPExtTag(major, osName, extSuffix, phpType);
}

export function isError(error: unknown): error is Error {
if (error && typeof error === "object" && "message" in error) {
return true;
Expand Down
12 changes: 12 additions & 0 deletions .github/actions/extract-image-context-action/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ export type ImageContext = {
dockerFile: string,
phpTag: string,
phpExtTag: string,
phpExtMinorTag: string,
phpExtMajorTag: string,
extList: string,
moveMinor: boolean,
moveMajor: boolean,
latest: boolean
}

export type RetagContext = {
phpExtTag: string,
phpExtMinorTag: string,
phpExtMajorTag: string,
retagMinor: boolean,
retagMajor: boolean,
}
Loading