diff --git a/action.yml b/action.yml index d183de5..9e5e8e5 100644 --- a/action.yml +++ b/action.yml @@ -27,5 +27,5 @@ outputs: package-path: description: 'Path to the generated .zip package' runs: - using: 'node20' + using: 'node24' main: dist/index.js diff --git a/src/index.js b/src/index.js index 19f23c2..aaef377 100644 --- a/src/index.js +++ b/src/index.js @@ -37,28 +37,6 @@ async function execInDocker(command, args = [], opts = {}) { return exec.getExecOutput(command, args, opts); } -/** - * Run a command via exec.exec (no output capture) either on host or in Docker. - */ -async function execRunInDocker(command, args = [], opts = {}) { - const dockerImage = getDockerImage(); - if (dockerImage) { - const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); - const cwd = opts.cwd ? path.resolve(opts.cwd) : workspace; - const workDir = cwd.startsWith(workspace) - ? `/workspace${cwd.substring(workspace.length)}` - : "/workspace"; - - return exec.exec("docker", [ - "run", "--rm", - "-v", `${workspace}:/workspace`, - "-w", workDir, - dockerImage, - command, ...args, - ]); - } - return exec.exec(command, args, opts); -} async function determineExtensionNameFromComposerJson() { core.info("Detecting extension name from composer.json..."); @@ -312,13 +290,40 @@ async function extensionDetails() { return { releaseTag: releaseTag, + extName: extName, extSoFile: `${extName}.so`, extPackageName: `php_${extName}-${releaseTag}_php${phpMajorMinor}-${arch}-${os}-${libcFlavour}${zendDebug}${ztsMode}.zip` }; } +async function smokeTest(extName, soPath) { + core.info("Smoke testing extension..."); + const phpBinary = await module.exports.determinePhpBinary(); + const dockerImage = getDockerImage(); + + if (dockerImage) { + // Docker: fresh container may need runtime deps (libstdc++/libgcc for C++ extensions) + const result = await execInDocker("sh", [ + "-c", + `if command -v apk >/dev/null 2>&1; then apk add --no-cache libstdc++ libgcc; fi && ${phpBinary} -d extension=${soPath} -r "echo extension_loaded('${extName}') ? 'OK' : 'FAIL';"`, + ]); + if (!result.stdout.includes("OK")) { + throw new Error(`Smoke test failed: extension '${extName}' did not load`); + } + } else { + const result = await execInDocker(phpBinary, [ + "-d", `extension=${soPath}`, + "-r", `echo extension_loaded('${extName}') ? 'OK' : 'FAIL';`, + ]); + if (!result.stdout.includes("OK")) { + throw new Error(`Smoke test failed: extension '${extName}' did not load`); + } + } + core.info("Smoke test passed!"); +} + async function main() { - const { releaseTag, extSoFile, extPackageName } = await module.exports.extensionDetails(); + const { releaseTag, extName, extSoFile, extPackageName } = await module.exports.extensionDetails(); await module.exports.buildExtension(); @@ -326,6 +331,9 @@ async function main() { const modulesDir = path.join(buildPath, "modules"); await exec.exec("ls", ["-l", modulesDir]); + const soPath = path.join(modulesDir, extSoFile); + await module.exports.smokeTest(extName, soPath); + await exec.exec("zip", ["-j", extPackageName, path.join(modulesDir, extSoFile)]); await module.exports.uploadReleaseAsset(releaseTag, extPackageName); @@ -345,6 +353,7 @@ module.exports = { determinePhpDebugMode, determineZendThreadSafeMode, uploadReleaseAsset, + smokeTest, extensionDetails, main, }; diff --git a/tests/index.test.js b/tests/index.test.js index f482254..f90a6dd 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -552,6 +552,7 @@ describe('extensionDetails', () => { expect(await action.extensionDetails()) .toEqual({ releaseTag: '1.2.3', + extName: 'foo', extSoFile: 'foo.so', extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc.zip', }); @@ -572,6 +573,7 @@ describe('extensionDetails', () => { expect(await action.extensionDetails()) .toEqual({ releaseTag: '1.2.3', + extName: 'foo', extSoFile: 'foo.so', extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip', }); @@ -582,10 +584,12 @@ describe('main', () => { test('main builds and uploads extension with default build path', async () => { jest.spyOn(action, 'extensionDetails').mockResolvedValue({ releaseTag: '1.2.3', + extName: 'foo', extSoFile: 'foo.so', extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip', }); jest.spyOn(action, 'buildExtension').mockResolvedValue(); + jest.spyOn(action, 'smokeTest').mockResolvedValue(); jest.spyOn(action, 'uploadReleaseAsset').mockResolvedValue(); jest.spyOn(exec, 'exec').mockResolvedValue(); core.getInput.mockImplementation((name) => { @@ -605,10 +609,12 @@ describe('main', () => { test('main builds and uploads extension with custom build path', async () => { jest.spyOn(action, 'extensionDetails').mockResolvedValue({ releaseTag: '1.2.3', + extName: 'foo', extSoFile: 'foo.so', extPackageName: 'php_foo-1.2.3_php8.1-x86_64-linux-glibc-debug-zts.zip', }); jest.spyOn(action, 'buildExtension').mockResolvedValue(); + jest.spyOn(action, 'smokeTest').mockResolvedValue(); jest.spyOn(action, 'uploadReleaseAsset').mockResolvedValue(); jest.spyOn(exec, 'exec').mockResolvedValue(); core.getInput.mockImplementation((name) => {