How to Set up Mailgun configuration to email newsletters for your Ghost blog

cy.request with FormData and QueryString

2023, new start! new ghost!

Share files between different agent workspace in the same Jenkins Pipeline via stash|unstash

 


Share files between different agent workspace in the same Jenkins Pipeline via stash|unstash
Photo by The Halal Design Studio / Unsplash

I was working on a legacy app recently. The legacy app has angular as its frontend and spring boot app as its backend. Originally the frontend build outputs are committed in the app's git repository and packaged by spring boot by copying them to the ${project.basedir}/target/classes/static/ class path(since content under the folder will be served as static resources by spring boot). And we are using Jenkins pipeline to run the build for this app. Hence the pipeline only use a java jenkins slave to compile and package the spring boot app. But that is not the right way, isn't it?

We should run both build the pipeline for frontend angular app and backend sprint boot app. But an angular app required a node slave to build it out. We don't have a Jenkins slave that has both java and node installed. Then issue comes: How to build java and javascript apps in a single Jenkins pipeline?

Then I found Pipeline allows utilizing multiple agents in the Jenkins environment from within the sameJenkinsfile, which can helpful for more advanced use-cases such as executing builds/tests across multiple platforms.

utilizing multiple agents in Pipeline

Good. We could use node agent to build the angular app and use java agent to compile the spring boot app.

    agent {
        label 'maven' # agent can build java app
    }
    stages {
        stage('Build UI') {
            agent {
                label 'node8' # agent can build javascript app
            }
            steps {
                sh 'npm install'
                sh 'run build'
            }
        }
        stage('Compile') {
            // Build the code to generate the spring-boot jar
            steps {
                sh "mvn -B clean install -f ${POM_FILE}"
            }
        }
    }

The above snippet works well, both frontend and backend got built out. I encountered another challenge: Each agent will run the steps in its own container, which means we are not able to access the dist build out from node agent in the maven agent, since they are using different workspaces.

Share files between agents

While I was searching a proper way to address the above concern, I found stash and unstash directives:

stash: Stash some files to be used later in the build
unstash: Restore files previously stashed

Jenkinsfile

pipeline {
    agent {
        label 'maven'
    }
    environment {
        POM_FILE 'server/pom.xml'
    }
    stages {
        stage('Build UI') {
            agent {
                label 'node8'
            }
            steps {
                sh 'npm --prefix=ui install'
                sh 'npm --prefix=ui run build'
                stash includes: 'ui/dist/**', name: 'uidist'
            }
        }
        stage('Compile') {
            // Build the code to generate the spring-boot jar
            steps {
                sh "mvn -B clean install -f ${POM_FILE}"
            }
        }
        stage('Package') {
            // Build the code to generate the spring-boot jar
            steps {
                unstash "uidist"
                sh "ls -all ui/dist"
                sh "mvn -DskipTests=true -B package -f ${POM_FILE}"
            }
        }
    }
}
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.basedir}/target/classes/static/</outputDirectory>
<resources>
<resource>
<directory>../ui/dist</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>

References:


https://docs.cloudbees.com/docs/admin-resources/latest/automating-with-jenkinsfile/using-multiple-agents

What does a ws() block do in Jenkins?
I’m trying to convert few Jenkinsfiles from Scripted Pipeline to Declarative Pipeline. I have a block like this in a Jenkinsfile: ws(”/path/to/dir”) { // do stuff} I wonder what does it do e...
How to re-use previously created workspace across stages
I am facing an issue where I have two stages defined in my pipeline that both are run on the same node and need to be run in the same workspace. The first of these stages runs on my master node
Aggregating results of downstream parameterised jobs in Jenkins
I have a Jenkins Build job which triggers multiple Test jobs with the test name as a parameter using the Jenkins Parameterized Trigger Plugin. This kicks off a number of test builds on multiple exe...

Swagger annotations for API that allows downloading files as zip

 I have built out an API that allows you downloading log files as zip in a sprint boot app. The app also integrated with Swagger 3, so I also want this API can be hit in the Swagger UI. In this post, I will demonstrate how to use @ApiOperation annotation to tell Swagger UI download the response as a zip file.

use @ApiOperation for your method

The key is set media type to "application/octet-stream" which can be recognized by Swagger UI. By doing so, you are set "accept":"application/octet-stream" in the request header. Put below annotation section on top of your API's controller method.

@Operation(
    		summary = "",
    		description = "",
    		method="GET",
    		responses = {
                    @ApiResponse(responseCode = "200", description = "", content = {
                            @Content(
                                    mediaType = "application/octet-stream"
                            )
                    })
            })

You also need to set 'produces = {"application/octet-stream"}' in the Mapping annotation. This is to set the response's header with "Content-Type":"application/octet-stream".

@GetMapping(value = "/yourpath", produces = {"application/octet-stream"})

set response Header, "content-disposition": "attachment; filename=xxx.zip"

In a regular HTTP response, the Content-Disposition response header is a header indicating if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved locally.

@GetMapping(value = "/yourpath", produces = {"application/octet-stream"})
public void method(HttpServletResponse response) {
            response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment; filename=%s.zip", transactionId));
}

Try it out in Swagger UI

You will be able to see a 'Download file' anchor once the api successfully response you the zip file.
You also be able find in the response headers:
content-disposition: attachment; filename=xxx.zip

How to Extract|Compress zip files using Java

 import java.util.zip.ZipEntry;

import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

extract Zip

public class ZipExtract {

    public List<String> extractZipFile(MultipartFile zipFile) {
        try (val zipInput = new ZipInputStream(zipFile.getInputStream())) {
            List<String> logFiles = new ArrayList<>();
            ZipEntry zipEntry;
            log.info("Received Zip File: {}", zipFile.getOriginalFilename());
            while ((zipEntry = zipInput.getNextEntry()) != null) {
                log.info("Extracted Log File: {}", zipEntry.getName());
                logFiles.add(IOUtils.toString(zipInput, StandardCharsets.UTF_8));
            }
            return logFiles;
        } catch (IOException e) {
            log.error("Fail to extract the zip file!{}", zipFile.getOriginalFilename(),  e);
        }
        return Collections.emptyList();
    }

}

comprese Zip

public class ZipCompress {

    public ZipOutputStream compressZip(List<List<String>> messages, OutputStream outputStream) throws IOException {
        val zos = new ZipOutputStream(outputStream);
        for (int i = 0; i < messages.size(); i++) {
            List<String> message = messages.get(i);
            val ze = new ZipEntry(message.get(0) + ".xml");
            zos.putNextEntry(ze);
            zos.write(message.get(1).getBytes(StandardCharsets.UTF_8), 0, message.get(1).length());
            zos.closeEntry();
        }
        return zos;
    }

}

fixed: embedded-redis: Unable to run on macOS Sonoma

Issue you might see below error while trying to run embedded-redis for your testing on your macOS after you upgrade to Sonoma. java.la...