`

Jenkins插件开发笔记

阅读更多

第一次做Jenkins插件开发,遂将笔记公开分享

插件名称: gettingCase

插件功能: 获取RallyDev上的某一个Test Case信息

 

0.配置.m2/settings.xml

请查阅本文最后的参考资料

 

 1.Maven创建Jenkins插件项目

mvn -U org.jenkins-ci.tools:maven-hpi-plugin:create

第一次执行会比较慢,因为需要下载很多Maven插件.

这个创建项目的过程有2步互动:

第一步需要开发者输入Maven项目的groupId

Enter the groupId of your plugin [org.jenkins-ci.plugins]:

com.technicolor.qcs

第二步需要开发者输入Maven项目的artifactId

Enter the artifactId of your plugin (normally without '-plugin' suffix):

gettingCase

 

2.基于Hello World插件项目,修改以实现自己插件功能

2.1POM

修改pom.xml文件,增加REST访问RallyDev的工具包

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.jenkins-ci.plugins</groupId>
        <artifactId>plugin</artifactId>
        <version>1.509.3</version>
        <!-- which version of Jenkins is this plugin built against? -->
    </parent>

    <groupId>com.technicolor.qcs</groupId>
    <artifactId>gettingCase</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>hpi</packaging>
    <description>Gets Rally Test Cases</description>

    <developers>
        <developer>
            <id>feuyeux</id>
            <name>eric han</name>
            <email>feuyeux@gmail.com</email>
        </developer>
    </developers>
    <dependencies>
        <dependency>
            <groupId>com.rallydev.rest</groupId>
            <artifactId>rally-rest-api</artifactId>
            <version>2.0.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient-cache</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpmime</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>fluent-hc</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.1</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.6</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.1</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>repo.jenkins-ci.org</id>
            <url>http://repo.jenkins-ci.org/public/</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>repo.jenkins-ci.org</id>
            <url>http://repo.jenkins-ci.org/public/</url>
        </pluginRepository>
    </pluginRepositories>
</project>

 2.2 编写全局配置页面

/home/hanl/j-ci/gettingCase/src/main/resources/com/technicolor/qcs/gettingCase/GetCasesBuilder/global.jelly

 

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:section title="Getting Test Cases Builder">
    <f:entry title="RallyDev User Name" field="userName">
      <f:textbox />
    </f:entry>
    <f:entry title="RallyDev Password" field="password">
      <f:password/>
    </f:entry>

    <f:entry title="HTTP Proxy URL" field="proxyURL">
      <f:textbox />
    </f:entry>
    <f:entry title="RallyDev Proxy User Name" field="proxyUser">
      <f:textbox />
    </f:entry>
    <f:entry title="RallyDev Proxy Password" field="proxyPassword">
      <f:password />
    </f:entry>
  </f:section>
</j:jelly>
 

 2.3 编写JOB配置页面

1  app  Hudson应用程序对象
${app.displayName}. //应用的Display名称

2  it   当前UI所属的模型对象
${it.name} 对应于builder的getName()方法

3  h   一个全局的工具类,提供静态工具方法   

 

/home/hanl/j-ci/gettingCase/src/main/resources/com/technicolor/qcs/gettingCase/GetCasesBuilder/config.jelly

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="Test Case No." field="testCaseId">
    <f:textbox />
  </f:entry>
</j:jelly>

 

2.4 编写扩展点方法

 

一次构建过程通常包括:
SCM checkout - check out出源码
Pre-build    - 预编译
Build wrapper  -准备构建的环境,设置环境变量等
Builder runs   - 执行构建,比如调用calling Ant, Make
Recording    - 记录输出,如测试结果
Notification    - 通知成员

 

/home/hanl/j-ci/gettingCase/src/main/java/com/technicolor/qcs/gettingCase/GetCasesBuilder.java

package com.technicolor.qcs.gettingCase;

import hudson.Launcher;
import hudson.Extension;
import hudson.util.FormValidation;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.AbstractProject;
import hudson.tasks.Builder;
import hudson.tasks.BuildStepDescriptor;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.QueryParameter;

import javax.servlet.ServletException;
import java.io.IOException;
import java.io.PrintStream;

/**
 * author:feuyeux
 */
public class GetCasesBuilder extends Builder {
    public static final String RALLY_URL = "https://rally1.rallydev.com";
    private final String testCaseId;

    @DataBoundConstructor
    public GetCasesBuilder(String testCaseId) {
        this.testCaseId = testCaseId;
    }

    public String getTestCaseId() {
        return testCaseId;
    }

    @Override
    public boolean perform(AbstractBuild build, Launcher launcher, BuildListener listener) {
        PrintStream out = listener.getLogger();
        final String userName = getDescriptor().getUserName();
        final String password = getDescriptor().getPassword();
        final String proxyURL = getDescriptor().getProxyURL();
        final String proxyUser = getDescriptor().getProxyUser();
        final String proxyPassword = getDescriptor().getProxyPassword();

        out.println("RallyDev User Name =" + userName);
        out.println("HTTP Proxy=" + proxyUser + "@" + proxyURL);
        out.println("RallyDev Test Case =" + getTestCaseId()+"\n");
        RallyClient rallyClient = null;
        try {
            rallyClient = new RallyClient(RALLY_URL, userName, password, proxyURL, proxyUser, proxyPassword);
            String caseInfo = rallyClient.getTestCases(testCaseId);
            out.println(caseInfo);
        } catch (Exception e) {
            out.println(e.getMessage());
        } finally {
            try {
                rallyClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        private String userName;
        private String password;
        private String proxyURL;
        private String proxyUser;
        private String proxyPassword;

        public FormValidation doCheckName(@QueryParameter String value)
                throws IOException, ServletException {
            if (value.length() == 0)
                return FormValidation.error("Please set a testCaseId");
            return FormValidation.ok();
        }

        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }

        public String getDisplayName() {
            return "Getting Test cases from Rally";
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            userName = formData.getString("userName");
            password = formData.getString("password");
            proxyURL = formData.getString("proxyURL");
            proxyUser = formData.getString("proxyUser");
            proxyPassword = formData.getString("proxyPassword");
            save();
            return super.configure(req, formData);
        }

        public String getUserName() {
            return userName;
        }

        public String getPassword() {
            return password;
        }

        public String getProxyURL() {
            return proxyURL;
        }

        public String getProxyUser() {
            return proxyUser;
        }

        public String getProxyPassword() {
            return proxyPassword;
        }
    }
}

 2.5 编写RallyDev连接和访问方法

/home/hanl/j-ci/gettingCase/src/main/java/com/technicolor/qcs/gettingCase/RallyClient.java

package com.technicolor.qcs.gettingCase;

import com.google.gson.JsonObject;
import com.rallydev.rest.RallyRestApi;
import com.rallydev.rest.request.GetRequest;
import com.rallydev.rest.response.GetResponse;
import com.rallydev.rest.response.Response;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

/**
 * author:feuyeux
 */
public class RallyClient {
    /*https://github.com/RallyTools/RallyRestToolkitForJavahttps://github.com/RallyTools/RallyRestToolkitForJava */
    private final RallyRestApi restApi;

    public RallyClient(String rally_url, String userName, String password, String proxyURL, String proxyUser, String proxyPassword) throws URISyntaxException {
        restApi = new RallyRestApi(new URI(rally_url), userName, password);
        restApi.setProxy(new URI(proxyURL), proxyUser, proxyPassword);
    }

    public void close() throws IOException {
        restApi.close();
    }

    public String getTestCases(String testCaseId) {
        /*https://rally1.rallydev.com/slm/doc/webservice/*/
        StringBuilder result = new StringBuilder();

        String version = restApi.getWsapiVersion();
        result.append("RallyDev Rest Version=").append(version).append("\n");

        final String query = "/testcase/"+testCaseId;
        GetRequest queryRequest = new GetRequest(query);
        GetResponse casesResponse = null;
        try {
            casesResponse = restApi.get(queryRequest);
        } catch (IOException e) {
            e.printStackTrace();
        }
        printWarningsOrErrors(casesResponse, result);
        JsonObject caseJsonObject = casesResponse.getObject();

        result.append("\n").append("Test Case Name: ").append(caseJsonObject.get("Name").getAsString());
        result.append("\n").append("Test Case Type: ").append(caseJsonObject.get("Type").getAsString());
        result.append("\n").append("Test Case URL: ").append(caseJsonObject.get("_ref").getAsString());
        result.append("\n").append("Test Case Creation Time: ").append(caseJsonObject.get("CreationDate").getAsString());
        result.append("\n").append("Test Case LastUpdate Time: ").append(caseJsonObject.get("LastUpdateDate").getAsString());
        result.append("\n").append("Test Case's Project: ").append( caseJsonObject.get("Project").getAsJsonObject().get("_refObjectName")    .getAsString());
        result.append("\n").append("Test Case's Workspace: ").append( caseJsonObject.get("Workspace").getAsJsonObject().get("_refObjectName")    .getAsString());

        return result.toString();
    }

    private void printWarningsOrErrors(Response response, StringBuilder result) {
        if (response.wasSuccessful()) {
            result.append("\nSuccess.");
            String[] warningList;
            warningList = response.getWarnings();
            for (int i = 0; i < warningList.length; i++) {
                result.append("\twarning:\n" + warningList[i]);
            }
        } else {
            String[] errorList;
            errorList = response.getErrors();
            if (errorList.length > 0) {
                result.append("\nError.");

            }
            for (int i = 0; i < errorList.length; i++) {
                result.append("\terror:\n" + errorList[i]);
            }
        }
    }
}

 3.调试Plugin程序

在终端/控制台,首先执行Maven变量配置命令

set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,address=8000,suspend=n

cd到插件项目目录,执行如下命令

hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvn clean
hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvnDebug hpi:run

 Maven将对8000端口执行监听,以便在IDE中进行断点调试

Preparing to Execute Maven in Debug Mode
Listening for transport dt_socket at address: 8000

 

进入IDE(本例使用IntelliJ),选择Run菜单的Debug,添加一个8000端口的远程服务器

 

添加断点,点击左下角运行调试按钮

此时,终端将执行Maven构建,并启动Jetty服务器.
hanl@hanl-ubuntu1204:~/j-ci/gettingCase$ mvnDebug hpi:run
Preparing to Execute Maven in Debug Mode
Listening for transport dt_socket at address: 8000
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building gettingCase 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] >>> maven-hpi-plugin:1.95:run (default-cli) @ gettingCase >>>
[INFO] 
[INFO] --- maven-hpi-plugin:1.95:validate (default-validate) @ gettingCase ---
[INFO] 
[INFO] --- maven-enforcer-plugin:1.0.1:enforce (enforce-maven) @ gettingCase ---
[INFO] 
[INFO] --- maven-enforcer-plugin:1.0.1:display-info (display-info) @ gettingCase ---
[INFO] Maven Version: 3.0.4
[INFO] JDK Version: 1.7.0_25 normalized as: 1.7.0-25
[INFO] OS Info: Arch: amd64 Family: unix Name: linux Version: 3.2.0-54-generic
[INFO] 
[INFO] --- maven-localizer-plugin:1.14:generate (default) @ gettingCase ---
[INFO] 
[INFO] --- maven-resources-plugin:2.5:resources (default-resources) @ gettingCase ---
[debug] execute contextualize
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 5 resources
[INFO] 
[INFO] --- maven-compiler-plugin:2.5:compile (default-compile) @ gettingCase ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] <<< maven-hpi-plugin:1.95:run (default-cli) @ gettingCase <<<
[INFO] 
[INFO] --- maven-hpi-plugin:1.95:run (default-cli) @ gettingCase ---
[INFO] Generating ./work/plugins/gettingCase.hpl
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/mailer/1.4/mailer-1.4.jar
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/ant/1.1/ant-1.1.jar
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/main/maven-plugin/1.509.3/maven-plugin-1.509.3.jar
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/javadoc/1.0/javadoc-1.0.jar
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/plugins/subversion/1.26/subversion-1.26.jar
[INFO] Copying dependency Jenkins plugin /home/hanl/.m2/repository/org/jenkins-ci/main/ui-samples-plugin/1.509.3/ui-samples-plugin-1.509.3.jar
[INFO] Configuring Jetty for project: gettingCase
2013-10-10 18:10:55.444::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /jenkins
[INFO] Tmp directory = /home/hanl/j-ci/gettingCase/target/work
[INFO] Web defaults =  jetty default
[INFO] Starting jetty 6.1.1 ...
2013-10-10 18:10:55.587::INFO:  jetty-6.1.1
Jenkins home directory: /home/hanl/j-ci/gettingCase/./work found at: System.getProperty("HUDSON_HOME")
2013-10-10 18:10:59.572::INFO:  Started SelectChannelConnector @ 0.0.0.0:8080
[INFO] Started Jetty Server
[INFO] Console reloading is ENABLED. Hit ENTER on the console to restart the context.
Oct 10, 2013 6:10:59 PM jenkins.InitReactorRunner$1 onAttained
INFO: Started initialization
Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained
INFO: Listed all plugins
Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained
INFO: Prepared all plugins
Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained
INFO: Started all plugins
Oct 10, 2013 6:11:01 PM jenkins.InitReactorRunner$1 onAttained
INFO: Augmented all extensions
Oct 10, 2013 6:11:04 PM jenkins.InitReactorRunner$1 onAttained
INFO: Loaded all jobs
Oct 10, 2013 6:11:05 PM org.jenkinsci.main.modules.sshd.SSHD start
INFO: Started SSHD at port 54676
Oct 10, 2013 6:11:05 PM jenkins.InitReactorRunner$1 onAttained
INFO: Completed initialization
Oct 10, 2013 6:11:05 PM hudson.TcpSlaveAgentListener <init>
INFO: JNLP slave agent listener started on TCP port 47342
Oct 10, 2013 6:11:05 PM hudson.WebAppMain$2 run
INFO: Jenkins is fully up and running

4.测试

在浏览器中录入Jenkins地址,创建Job并按照上述的配置环节,完成配置.

执行Build Job
此时,断点应其作用,可以在IDE中进行调试和监控.

当完成调试后,进入Jenkins构建结果页面,观察构建结果是否符合预期.

 

到此,获取并显示RallyDev中Test Case的Jenkins插件开发完毕.

 

IntelliJ IDE 插件:

https://wiki.jenkins-ci.org/display/JENKINS/IntelliJ+IDEA+plugin+for+Stapler

Stapler plugin for IntelliJ IDEA

 

参考资料

https://wiki.jenkins-ci.org/display/JENKINS/Plugin+tutorial

https://github.com/jenkinsci/hello-world-plugin

https://jenkins-ci.org/maven-site/jenkins-core/jelly-taglib-ref.html

ui-samples: http://localhost:8080/jenkins/ui-samples/

扩展点:https://wiki.jenkins-ci.org/display/JENKINS/Extension+points

 

 

<f:entry title="Test Station">
<select class="setting-input" name="AndroidBuilder.stationUrl">
    <j:forEach var="inst" items="${descriptor.stations}">
        <f:option selected="${inst.url==instance.stationUrl}">${inst.url}</f:option>
    </j:forEach>
</select>
</f:entry>

 

分享到:
评论

相关推荐

    jenkins插件开发笔记

    第一次做Jenkins插件开发,遂将笔记公开分享插件名称:gettingCase插件功能:获取RallyDev上的某一个TestCase信息请查阅本文最后的参考资料mvn-Uorg.jenkins-ci.tools:maven-hpi-plugin:create第一次执行会比较慢,因为...

    Jenkins笔记笔记笔记

    【Jenkins笔记笔记笔记】 Jenkins 是一个开源的持续集成(CI)服务器,它使得开发者能够在软件开发过程中实现自动化构建、测试和部署。这个工具在IT行业中被广泛使用,尤其在敏捷开发和DevOps实践中扮演着重要角色...

    Jenkins 持续集成学习笔记.pdf

    【Jenkins 持续集成学习笔记】 Jenkins 是一款广泛应用的开源持续集成工具,它支持自动化的构建、测试和部署流程,以提高软件开发效率和质量。在现代软件开发中,持续集成(CI)是敏捷开发的重要实践之一,它强调...

    jenkins课件笔记

    Jenkins 是一个开源的持续集成(CI)服务器,用于自动化各种软件开发过程,如构建、测试和部署。它支持多种版本控制系统,并且可以通过插件扩展其功能,使之适用于各种软件开发环境。下面将详细阐述 Jenkins 的核心...

    jenkins安装文件及教程

    在IT领域,Jenkins因其易用性和丰富的插件生态而备受青睐。以下是关于"jenkins安装文件及教程"的相关知识点: 一、Jenkins安装 1. **RPM安装**:描述中提到的`jenkins-1.658-1.1.noarch.rpm`是一个基于RPM的Jenkins...

    Jenkins+SVN+Ant持续集成环境配置笔记

    ### Jenkins+SVN+Ant持续集成环境配置笔记 #### 一、引言 随着软件开发项目的日益复杂化,持续集成(Continuous Integration, CI)已成为现代软件工程中的一个重要组成部分。通过持续集成,团队能够频繁地集成代码...

    全网最全Android开发笔记.zip

    【Android开发笔记】是一部全面覆盖Android开发核心技术与实践的综合资料,旨在帮助开发者从零基础到精通,深入了解Android系统的工作原理以及应用开发的各种技巧。笔记包含了大量的实例代码、技术解析和实战经验,...

    Jenkins+SVN+Ant持续集成环境配置笔记.pdf

    Jenkins是一种开源的持续集成和持续交付的自动化服务器,可以帮助开发者快速发现和解决缺陷,提高软件质量并缩短发布周期。SVN(Subversion)是一种版本控制系统,用于管理源代码的版本历史,支持开发者团队协作开发...

    eclipse插件开发资料

    本资料包提供了两个核心资源,即"Eclipse 3.6 API.chm"和"Eclipse插件开发学习笔记.pdf",它们将帮助我们了解和实践Eclipse插件开发。 "Eclipse 3.6 API.chm"是Eclipse 3.6版本的API文档,包含了所有可用的类、接口...

    mavenpaper-m开发笔记

    【标题】"mavenpaper-m开发笔记"指出这是一份关于使用Maven进行软件开发的文档,Maven是Java世界中广泛使用的项目管理和综合工具。它通过读取项目的配置文件(pom.xml)来管理项目的构建、依赖和报告。这份笔记可能...

    jenkins

    它的灵活性和强大的插件生态系统使得它在软件开发过程中扮演着至关重要的角色。通过定时触发构建,Jenkins 可以帮助团队快速发现代码集成中的问题,从而提高开发效率和软件质量。 首先,Jenkins 的核心功能在于源码...

    pycharm官网paper-m开发笔记

    【标题】"pycharm官网paper-m开发笔记"指的是在PyCharm官网上关于"paper-m"项目的开发经验、技巧和笔记。PyCharm是一款由JetBrains公司开发的强大Python集成开发环境(IDE),它提供了丰富的功能,如代码编辑、调试...

    mavenCM-2023C-开发笔记

    【描述】"mavenCM-2023C-开发笔记"的描述较为简洁,但可以推断笔记可能涵盖了如何配置Maven项目、管理依赖、执行构建任务、使用插件等内容。由于没有具体细节,我们可以预期笔记会包含开发者在实际工作中遇到的问题...

    gitlab10.6.3+jenkins2.164.3-k8s-1.14.2-cicd-java项目-jenkins在k8s之外

    这个压缩包包含了相关的安装包和笔记,帮助用户在Kubernetes集群外部设置Jenkins服务器,实现自动化构建和部署。 首先,让我们了解一下GitLab。GitLab是一个开源的版本控制系统,它提供了代码托管、代码审查、项目...

    Eclipse 开发学习笔记.pdf

    11. **插件开发基础**:讲解插件开发的基本概念,如Plug-in ID、Bundle、Extension Point和Extension。 六、团队协作 12. **团队协作工具**:如Mylyn任务管理,与持续集成服务器如Jenkins的集成,协同开发的最佳...

    Ant 整合Jenkins自动构建

    标题中的“Ant整合Jenkins自动构建”涉及到两个关键的开源工具——Apache Ant和Jenkins,它们在软件开发的持续集成(Continuous Integration, CI)流程中起着重要作用。 Apache Ant是Java平台上的一个构建工具,...

    vue开发CM-2023C-ma开发笔记

    在"vue开发CM-2023C-ma开发笔记"中,我们可以推测这是一份关于使用Vue.js进行特定项目“CM-2023C-ma”的开发记录。 1. **Vue.js基础** - **安装**:Vue.js可以通过CDN链接直接引入,或者使用npm或yarn全局或局部...

    【Jenkins学习笔记】玩转持续集成与持续交付

    【Jenkins学习笔记】玩转持续集成与持续交付 Jenkins是开源软件项目中的一个重要工具,专注于持续集成和持续交付。作为一个基于Java开发的可拓展平台,它的主要功能包括自动构建、测试和集成软件项目,同时监控定时...

Global site tag (gtag.js) - Google Analytics