You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React应用无法获取Jenkins Pipeline状态及相关问题求助

问题定位与解决请求

我正在开发一个React应用,通过Jenkins Pipeline完成以下流程:克隆代码仓库、运行MegaLinter,流程结束后清理megalinter.log文件并在Issues.jsx页面展示检测出的问题。Pipeline构建过程中会显示动画状态,但遇到了以下问题:

  • 即使Pipeline触发成功,几秒后控制台仍出现错误;
  • 构建完成后无法获取工件;
  • 动画状态很快显示失败,但Jenkins中Pipeline已正常触发,不清楚错误原因。
    另外,我未使用包含147的URL,但该请求出现错误。以下是相关代码,寻求问题定位与解决方法。

Jenkins Pipeline代码

pipeline {
    agent any

    environment {
        NODEJS_HOME = tool name: 'NodeJS', type: 'jenkins.plugins.nodejs.tools.NodeJSInstallation'
        PATH = "${env.NODEJS_HOME}/bin:${env.PATH}"
    }

    parameters {
        string(name: 'REPO_URL', defaultValue: '', description: 'URL of the Git repository to analyze')
    }

    stages {
        stage('Clone Repository') {
            steps {
                script {
                    try {
                        git url: "${params.REPO_URL}", branch: 'main'
                    } catch (Exception e) {
                        echo "Main branch not found, trying master branch"
                        git url: "${params.REPO_URL}", branch: 'master'
                    }
                }
            }
        }
        stage('Run MegaLinter') {
            steps {
                script {
                    try {
                        bat "npx mega-linter-runner --flavor cupcake --release v6"
                    } catch (Exception e) {
                        echo "MegaLinter found issues, but continuing pipeline"
                    }
                }
            }
        }
    }

    post {
        always {
            archiveArtifacts artifacts: 'megalinter-reports/**', allowEmptyArchive: true
            cleanWs()
        }
    }
}

Issues.jsx页面代码

const Issues = () => {
  const { repoName } = useParams();
  const [buildStage, setBuildStage] = useState('Initializing');
  const [issues, setIssues] = useState([]);
  const [filteredIssues, setFilteredIssues] = useState([]);
  const [filters, setFilters] = useState({
    severity: "",
    language: "",
    linter: ""
  });

  useEffect(() => {
    let isMounted = true;
    let intervalId;
  
    const runPipeline = async () => {
      try {
        setBuildStage('Triggering Pipeline');
        await triggerPipeline({ name: repoName });
        
        if (!isMounted) return;
        setBuildStage('Waiting for Build');
        
        let lastBuildNumber = null;
  
        const pollBuildStatus = async () => {
          try {
            const { isBuilding, result, number } = await checkBuildStatus();
            
            if (number !== lastBuildNumber) {
              lastBuildNumber = number;
              console.log(`New build started: #${number}`);
            }
  
            if (!isBuilding && result) {
              clearInterval(intervalId);
              if (result === 'SUCCESS') {
                if (isMounted) {
                  setBuildStage('Fetching Results');
                  const results = await getBuildResults(number);
                  await processMegaLinterResults(results.megaLinterLog);
                  setBuildStage('Complete');
                }
              } else {
                if (isMounted) setBuildStage(`Failed: ${result}`);
              }
            }
          } catch (error) {
            console.error('Error in pollBuildStatus:', error);
            if (isMounted) setBuildStage('Failed: Error checking status');
          }
        };
  
        intervalId = setInterval(pollBuildStatus, 5000); // Check every 5 seconds
      } catch (error) {
        console.error('Error in pipeline process:', error);
        if (isMounted) setBuildStage('Failed: Error triggering pipeline');
      }
    };
  
    runPipeline();
  
    return () => {
      isMounted = false;
      if (intervalId) clearInterval(intervalId);
    };
  }, [repoName]);

  const processMegaLinterResults = (logContent) => {
    try {
      const processedIssues = [];
      const lines = logContent.split('\n');
      
      let currentLinter = '';
      let currentFile = '';
  
      for (const line of lines) {
        if (line.startsWith('Linter:')) {
          currentLinter = line.split(':')[1].trim();
        } else if (line.startsWith('File:')) {
          currentFile = line.split(':')[1].trim();
        } else if (line.includes('|')) {
          const [lineNumber, column, level, message] = line.split('|').map(item => item.trim());
          processedIssues.push({
            message,
            linter: currentLinter,
            file: currentFile,
            severity: level,
            line: lineNumber,
            column
          });
        }
      }
  
      setIssues(processedIssues);
      setFilteredIssues(processedIssues);
    } catch (error) {
      console.error('Error processing MegaLinter results:', error);
    }
  };


  useEffect(() => {
    const filtered = issues.filter(issue => 
      (!filters.severity || issue.severity === filters.severity) &&
      (!filters.language || issue.language === filters.language) &&
      (!filters.linter || issue.linter === filters.linter)
    );
    setFilteredIssues(filtered);
  }, [issues, filters]);

  const handleFilterChange = (filterType, value) => {
    setFilters(prev => ({ ...prev, [filterType]: value }));
  };

  return (
    <div className='flex'>
      <Sidebar>
        <SideBarItem link="/user/dashboard" icon={<Home size={20} />} text="Home" alert />
        <SideBarItem link={`/user/dashboard/repos/${repoName}`} icon={<BarChart4 size={20} />} text="Details" alert />
        <SideBarItem link={`/user/dashboard/repos/${repoName}/issues`} icon={<Bug size={20} />} text="Issues" />
        <SideBarItem link={`/user/dashboard/repos/${repoName}/commits`} icon={<GitCommitVertical size={20} />} text="Commits" />
        <SideBarItem link={`/user/dashboard/repos/${repoName}/pull-requests`} icon={<GitPullRequestIcon size={20} />} text="Pull Requests" />
        <SideBarItem link="/user/dashboard/help" icon={<HandHelping size={20} />} text="Help" />
      </Sidebar>
      <div className="p-4 flex-grow">
        <h1 className="text-2xl font-bold mb-4">Issues of {repoName}</h1>
        {buildStage !== 'Complete' ? (
          <div className="flex flex-col items-center justify-center h-64">
            <div className="loader"></div>
            <p className="mt-4">Status: {buildStage}</p>
          </div>
        ) : (
          <div className="flex flex-col md:flex-row gap-4">
            <div className="flex-grow bg-black shadow-md rounded-lg p-6">
              <h2 className="text-xl font-semibold mb-4">Mega Linter Results</h2>
              <div className="overflow-x-auto">
                <table className="min-w-full bg-black">
                  <thead className="bg-gray-100">
                    <tr>
                      <th className="py-2 px-4 border-b text-left">S.No</th>
                      <th className="py-2 px-4 border-b text-left">Issues</th>
                      <th className="py-2 px-4 border-b text-left">Linter</th>
                      <th className="py-2 px-4 border-b text-left">File</th>
                      <th className="py-2 px-4 border-b text-left">Severity</th>
                    </tr>
                  </thead>
                  <tbody>
                    {filteredIssues.map((issue, index) => (
                      <tr key={index} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-black'}>
                        <td className="py-2 px-4 border-b">{index + 1}</td>
                        <td className="py-2 px-4 border-b">{issue.message}</td>
                        <td className="py-2 px-4 border-b">{issue.linter}</td>
                        <td className="py-2 px-4 border-b">{issue.file}</td>
                        <td className="py-2 px-4 border-b">{issue.severity}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </div>
            
            <div className="w-full md:w-64 bg-black shadow-md rounded-lg p-6">
              <h2 className="text-xl font-semibold mb-4">Filters</h2>
              <div className="space-y-4">
                <div>
                  <label htmlFor="severity" className="block text-sm font-medium text-gray-700">Severity</label>
                  <select
                    id="severity"
                    className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
                    onChange={(e) => handleFilterChange('severity', e.target.value)}
                  >
                    <option value="">Select severity</option>
                    {['Low', 'Medium', 'High'].map((option) => (
                      <option key={option} value={option}>{option}</option>
                    ))}
                  </select>
                </div>
                
                <div>
                  <label htmlFor="language" className="block text-sm font-medium text-gray-700">Language</label>
                  <select
                    id="language"
                    className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
                    onChange={(e) => handleFilterChange('language', e.target.value)}
                  >
                    <option value="">Select language</option>
                    {[...new Set(issues.map(issue => issue.language))].map((option) => (
                      <option key={option} value={option}>{option}</option>
                    ))}
                  </select>
                </div>
                
                <div>
                  <label htmlFor="linter" className="block text-sm font-medium text-gray-700">Linter</label>
                  <select
                    id="linter"
                    className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
                    onChange={(e) => handleFilterChange('linter', e.target.value)}
                  >
                    <option value="">Select linter</option>
                    {[...new Set(issues.map(issue => issue.linter))].map((option) => (
                      <option key={option} value={option}>{option}</option>
                    ))}
                  </select>
                </div>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default Issues;

控制器函数代码

export const triggerPipeline = async (repo) => {
  try {
    if (!repo || !repo.name) {
      throw new Error('Invalid repository information');
    }

    const { data: { user }, error } = await supabase.auth.getUser();
    if (error) {
      throw new Error('Failed to fetch user data');
    }

    const githubUsername = user.user_metadata.user_name;
    if (!githubUsername) {
      throw new Error('GitHub username not found in user metadata');
    }

    const response = await fetch('http://localhost:8080/job/generate-issues/buildWithParameters', {
      method: 'POST',
      headers: {
        'Authorization': 'Basic ' + btoa('username:password'),
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: `REPO_URL=https://github.com/${githubUsername}/${repo.name}.git`,
    });

    console.log('Response status:', response.status);
    console.log('Response headers:', JSON.stringify(Object.fromEntries(response.headers)));

    if (response.status === 201 || response.status === 303) {
      console.log('Jenkins pipeline triggered successfully');
      return { message: 'Pipeline triggered successfully' };
    } else {
      throw new Error(`Failed to trigger Jenkins pipeline: ${response.status}`);
    }
  } catch (error) {
    console.error('Error triggering Jenkins pipeline:', error);
    throw error;
  }
};

export const checkBuildStatus = async () => {
  const buildUrl = 'http://localhost:8080/job/generate-issues/lastBuild/api/json';
  const authHeader = 'Basic ' + btoa('username:password');

  try {
    const response = await fetch(buildUrl, {
      headers: {
        'Authorization': authHeader
      }
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch build status: ${response.status}`);
    }

    const buildData = await response.json();
    console.log('Build data:', buildData);

    return { 
      isBuilding: buildData.building,
      result: buildData.result,
      number: buildData.number
    };
  } catch (error) {
    console.error('Error checking build status:', error);
    return { isBuilding: false, result: 'ERROR', number: null };
  }
};

export const getBuildResults = async (buildNumber) => {
  const logUrl = `http://localhost:8080/job/generate-issues/${buildNumber}/artifact/megalinter-reports/megalinter.log`;
  const authHeader = 'Basic ' + btoa('username:password');

  try {
    const response = await fetch(logUrl, {
      headers: {
        'Authorization': authHeader
      }
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch MegaLinter log: ${response.status}`);
    }

    const megaLinterLog = await response.text();
    return { megaLinterLog };
  } catch (error) {
    console.error('Error fetching MegaLinter log:', error);
    throw error;
  }
};

问题分析与解决方法

1. 控制台触发成功后仍报错、动画状态显示失败

  • 原因:checkBuildStatus直接请求lastBuild,但Pipeline刚触发时新构建尚未生成,此时lastBuild指向旧构建,导致状态判断错误;且构建触发后立即轮询,新构建可能还未被Jenkins记录,返回的number为空,后续逻辑出错。
  • 解决:
    • 在triggerPipeline中解析Jenkins响应头的Location字段,提取新构建的编号并返回:
      if (response.status === 201 || response.status === 303) {
        const locationHeader = response.headers.get('Location');
        let buildNumber = null;
        if (locationHeader) {
          buildNumber = locationHeader.split('/').filter(part => !isNaN(part)).pop();
        }
        console.log('Jenkins pipeline triggered successfully');
        return { message: 'Pipeline triggered successfully', buildNumber };
      }
      
    • 在runPipeline中接收该编号,后续轮询和获取结果时直接使用此编号,避免依赖lastBuild:
      const { buildNumber } = await triggerPipeline({ name: repoName });
      // 修改pollBuildStatus,请求指定编号的构建状态
      const { isBuilding, result } = await checkBuildStatus(buildNumber);
      
    • 同步修改checkBuildStatus函数,支持传入构建编号:
      export const checkBuildStatus = async (buildNumber) => {
        const buildUrl = buildNumber 
          ? `http://localhost:8080/job/generate-issues/${buildNumber}/api/json`
          : 'http://localhost:8080/job/generate-issues/lastBuild/api/json';
        // 其余逻辑保持不变
      };
      

2. 构建完成后无法获取工件

  • 原因:MegaLinter未生成指定路径的日志文件,或Jenkins归档路径配置有误,或请求账号无工件读取权限。
  • 解决:
    • 查看Jenkins构建日志,确认megalinter-reports/megalinter.log是否生成;
    • 修改Pipeline归档步骤,明确指定日志文件路径:archiveArtifacts artifacts: 'megalinter-reports/megalinter.log', allowEmptyArchive: true;
    • 检查Jenkins账号权限,确保用于请求的账号拥有读取该任务工件的权限;
    • 在getBuildResults中打印完整请求URL,确认路径
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 07:07:02