diff --git a/.github/workflows/auto-pr-from-main.yml b/.github/workflows/auto-pr-from-main.yml index 5d30e73..b72eb89 100644 --- a/.github/workflows/auto-pr-from-main.yml +++ b/.github/workflows/auto-pr-from-main.yml @@ -40,7 +40,7 @@ jobs: git checkout -b $BRANCH_NAME # List outdated packages - dotnet list package --outdated + dotnet list MetarDecoder.sln package --outdated # Update packages (this is a simplified approach) echo "πŸ“¦ Updating NuGet packages..." @@ -154,10 +154,10 @@ jobs: echo "πŸ”’ Checking for security-related package updates..." # Check for vulnerable packages - dotnet list package --vulnerable + dotnet list MetarDecoder.sln package --vulnerable # Create update branch if vulnerabilities found - if dotnet list package --vulnerable | grep -q "vulnerable"; then + if dotnet list MetarDecoder.sln package --vulnerable | grep -q "vulnerable"; then echo "🚨 Security vulnerabilities detected, creating update PR..." BRANCH_NAME="security-update-$(date +%Y%m%d-%H%M%S)" diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e6f413d..bdc8bad 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -33,6 +33,8 @@ jobs: args: --linter,qodana-community-for-net --baseline,qodana.sarif.json --fail-threshold,0 cache-default-branch-only: true upload-result: false + primary-cache-key: qodana-2025.3-refs/heads/main-${{ github.sha }} + additional-cache-key: qodana-2025.3-refs/heads/main - name: πŸ“Š Upload Qodana Results uses: actions/upload-artifact@v4 diff --git a/.github/workflows/openhands-resolver.yml b/.github/workflows/openhands-resolver.yml index cf74ccc..37eeb78 100644 --- a/.github/workflows/openhands-resolver.yml +++ b/.github/workflows/openhands-resolver.yml @@ -1,7 +1,55 @@ -name: Resolve Issue with OpenHands +name: Auto-Fix Tagged Issue with OpenHands on: - workflow_dispatch: + workflow_call: + inputs: + max_iterations: + required: false + type: number + default: 50 + macro: + required: false + type: string + default: "@openhands-agent" + target_branch: + required: false + type: string + default: "main" + description: "Target branch to pull and create PR against" + pr_type: + required: false + type: string + default: "draft" + description: "The PR type that is going to be created (draft, ready)" + LLM_MODEL: + required: false + type: string + default: "anthropic/claude-sonnet-4-20250514" + LLM_API_VERSION: + required: false + type: string + default: "" + base_container_image: + required: false + type: string + default: "" + description: "Custom sandbox env" + runner: + required: false + type: string + default: "ubuntu-latest" + secrets: + LLM_MODEL: + required: false + LLM_API_KEY: + required: true + LLM_BASE_URL: + required: false + PAT_TOKEN: + required: false + PAT_USERNAME: + required: false + issues: types: [labeled] pull_request: @@ -19,17 +67,365 @@ permissions: issues: write jobs: - call-openhands-resolver: - uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main - with: - macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }} - max_iterations: ${{ fromJson(vars.OPENHANDS_MAX_ITER || 50) }} - base_container_image: ${{ vars.OPENHANDS_BASE_CONTAINER_IMAGE || '' }} - LLM_MODEL: ${{ vars.LLM_MODEL || 'gemini/gemini-2.0-flash' }} - target_branch: ${{ vars.TARGET_BRANCH || 'develop' }} - runner: ${{ vars.TARGET_RUNNER }} - secrets: - PAT_TOKEN: ${{ secrets.PAT_TOKEN }} - PAT_USERNAME: ${{ secrets.PAT_USERNAME || 'afonsoft' }} - LLM_API_KEY: ${{ secrets.LLM_API_KEY }} - LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} \ No newline at end of file + auto-fix: + if: | + github.event_name == 'workflow_call' || + github.event.label.name == 'fix-me' || + github.event.label.name == 'fix-me-experimental' || + ( + ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + contains(github.event.comment.body, inputs.macro || '@openhands-agent') && + (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER') + ) || + (github.event_name == 'pull_request_review' && + contains(github.event.review.body, inputs.macro || '@openhands-agent') && + (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER') + ) + ) + runs-on: "${{ inputs.runner || 'ubuntu-latest' }}" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + + - name: Get latest versions and create requirements.txt + run: | + python -m pip index versions openhands-ai > openhands_versions.txt + OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()') + + # Create a new requirements.txt locally within the workflow, ensuring no reference to the repo's file + echo "openhands-ai==${OPENHANDS_VERSION}" > /tmp/requirements.txt + cat /tmp/requirements.txt + + - name: Cache pip dependencies + if: | + !( + github.event.label.name == 'fix-me-experimental' || + ( + (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + contains(github.event.comment.body, '@openhands-agent-exp') + ) || + ( + github.event_name == 'pull_request_review' && + contains(github.event.review.body, '@openhands-agent-exp') + ) + ) + uses: actions/cache@v5 + with: + path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/* + key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('/tmp/requirements.txt') }} + + - name: Check required environment variables + env: + LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} + PAT_TOKEN: ${{ secrets.PAT_TOKEN }} + PAT_USERNAME: ${{ secrets.PAT_USERNAME }} + GITHUB_TOKEN: ${{ github.token }} + run: | + required_vars=("LLM_API_KEY") + for var in "${required_vars[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: Required environment variable $var is not set." + exit 1 + fi + done + + # Check optional variables and warn about fallbacks + if [ -z "$LLM_BASE_URL" ]; then + echo "Warning: LLM_BASE_URL is not set, will use default API endpoint" + fi + + if [ -z "$PAT_TOKEN" ]; then + echo "Warning: PAT_TOKEN is not set, falling back to GITHUB_TOKEN" + fi + + if [ -z "$PAT_USERNAME" ]; then + echo "Warning: PAT_USERNAME is not set, will use openhands-agent" + fi + + - name: Set environment variables + env: + REVIEW_BODY: ${{ github.event.review.body || '' }} + run: | + # Handle pull request events first + if [ -n "${{ github.event.pull_request.number }}" ]; then + echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + echo "ISSUE_TYPE=pr" >> $GITHUB_ENV + # Handle pull request review events + elif [ -n "$REVIEW_BODY" ]; then + echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV + echo "ISSUE_TYPE=pr" >> $GITHUB_ENV + # Handle issue comment events that reference a PR + elif [ -n "${{ github.event.issue.pull_request }}" ]; then + echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV + echo "ISSUE_TYPE=pr" >> $GITHUB_ENV + # Handle regular issue events + else + echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV + echo "ISSUE_TYPE=issue" >> $GITHUB_ENV + fi + + if [ -n "$REVIEW_BODY" ]; then + echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV + else + echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV + fi + + echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV + echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.PAT_TOKEN || github.token }}" >> $GITHUB_ENV + echo "SANDBOX_BASE_CONTAINER_IMAGE=${{ inputs.base_container_image }}" >> $GITHUB_ENV + + # Set branch variables + echo "TARGET_BRANCH=${{ inputs.target_branch || 'main' }}" >> $GITHUB_ENV + + - name: Comment on issue with start message + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const issueType = process.env.ISSUE_TYPE; + github.rest.issues.createComment({ + issue_number: ${{ env.ISSUE_NUMBER }}, + owner: context.repo.owner, + repo: context.repo.repo, + body: `[OpenHands](https://github.com/OpenHands/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).` + }); + + - name: Install OpenHands + id: install_openhands + uses: actions/github-script@v7 + env: + COMMENT_BODY: ${{ github.event.comment.body || '' }} + REVIEW_BODY: ${{ github.event.review.body || '' }} + LABEL_NAME: ${{ github.event.label.name || '' }} + EVENT_NAME: ${{ github.event_name }} + with: + script: | + const commentBody = process.env.COMMENT_BODY.trim(); + const reviewBody = process.env.REVIEW_BODY.trim(); + const labelName = process.env.LABEL_NAME.trim(); + const eventName = process.env.EVENT_NAME.trim(); + // Check conditions + const isExperimentalLabel = labelName === "fix-me-experimental"; + const isIssueCommentExperimental = + (eventName === "issue_comment" || eventName === "pull_request_review_comment") && + commentBody.includes("@openhands-agent-exp"); + const isReviewCommentExperimental = + eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp"); + + // Set output variable + core.setOutput('isExperimental', isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental); + + // Perform package installation + if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) { + console.log("Installing experimental OpenHands..."); + + await exec.exec("pip install git+https://github.com/openhands/openhands.git"); + } else { + console.log("Installing from requirements.txt..."); + + await exec.exec("pip install -r /tmp/requirements.txt"); + } + + - name: Attempt to resolve issue + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }} + GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} + GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} + LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} + PYTHONPATH: "" + run: | + cd /tmp && python -m openhands.resolver.resolve_issue \ + --selected-repo ${{ github.repository }} \ + --issue-number ${{ env.ISSUE_NUMBER }} \ + --issue-type ${{ env.ISSUE_TYPE }} \ + --max-iterations ${{ env.MAX_ITERATIONS }} \ + --comment-id ${{ env.COMMENT_ID }} \ + --is-experimental ${{ steps.install_openhands.outputs.isExperimental }} + + - name: Check resolution result + id: check_result + run: | + if cd /tmp && grep -q '"success":true' output/output.jsonl; then + echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT + else + echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT + fi + + - name: Upload output.jsonl as artifact + uses: actions/upload-artifact@v6 + if: always() # Upload even if previous steps fail + with: + name: resolver-output + path: /tmp/output/output.jsonl + retention-days: 30 # Keep artifact for 30 days + + - name: Create draft PR or push branch + if: always() # Create PR or branch even if previous steps fail + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || github.token }} + GITHUB_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} + GIT_USERNAME: ${{ secrets.PAT_USERNAME || 'openhands-agent' }} + LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }} + LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }} + PYTHONPATH: "" + run: | + if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then + cd /tmp && python -m openhands.resolver.send_pull_request \ + --issue-number ${{ env.ISSUE_NUMBER }} \ + --target-branch ${{ env.TARGET_BRANCH }} \ + --pr-type ${{ inputs.pr_type || 'draft' }} \ + --reviewer ${{ github.actor }} | tee pr_result.txt && \ + grep "PR created" pr_result.txt | sed 's/.*///g' > pr_number.txt + else + cd /tmp && python -m openhands.resolver.send_pull_request \ + --issue-number ${{ env.ISSUE_NUMBER }} \ + --pr-type branch \ + --send-on-failure | tee branch_result.txt && \ + grep "branch created" branch_result.txt | sed 's/.*///g; s/.expand=1//g' > branch_name.txt + fi + + # Step leaves comment for when agent is invoked on PR + - name: Analyze Push Logs (Updated PR or No Changes) # Skip comment if PR update was successful OR leave comment if agent made no code changes + uses: actions/github-script@v7 + if: always() + env: + AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }} + ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const fs = require('fs'); + const issueNumber = process.env.ISSUE_NUMBER; + let logContent = ''; + + try { + logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim(); + } catch (error) { + console.error('Error reading pr_result.txt file:', error); + } + + const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`; + + // Check logs from send_pull_request.py (pushes code to GitHub) + if (logContent.includes("Updated pull request")) { + console.log("Updated pull request found. Skipping comment."); + process.env.AGENT_RESPONDED = 'true'; + } else if (logContent.includes(noChangesMessage)) { + github.rest.issues.createComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.` + }); + process.env.AGENT_RESPONDED = 'true'; + } + + # Step leaves comment for when agent is invoked on issue + - name: Comment on issue # Comment link to either PR or branch created by agent + uses: actions/github-script@v7 + if: always() # Comment on issue even if previous steps fail + env: + AGENT_RESPONDED: ${{ env.AGENT_RESPONDED || 'false' }} + ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} + RESOLUTION_SUCCESS: ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }} + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const fs = require('fs'); + const path = require('path'); + const issueNumber = process.env.ISSUE_NUMBER; + const success = process.env.RESOLUTION_SUCCESS === 'true'; + + let prNumber = ''; + let branchName = ''; + let resultExplanation = ''; + + try { + if (success) { + prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim(); + } else { + branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim(); + } + } catch (error) { + console.error('Error reading file:', error); + } + + try { + if (!success){ + // Read result_explanation from JSON file for failed resolution + const outputFilePath = path.resolve('/tmp/output/output.jsonl'); + if (fs.existsSync(outputFilePath)) { + const outputContent = fs.readFileSync(outputFilePath, 'utf8'); + const jsonLines = outputContent.split('\n').filter(line => line.trim() !== ''); + + if (jsonLines.length > 0) { + // First entry in JSON lines has been key 'result_explanation' + const firstEntry = JSON.parse(jsonLines[0]); + resultExplanation = firstEntry.result_explanation || ''; + } + } + } + } catch (error){ + console.error('Error reading file:', error); + } + + // Check "success" log from resolver output + if (success && prNumber) { + github.rest.issues.createComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.` + }); + process.env.AGENT_RESPONDED = 'true'; + } else if (!success && branchName) { + let commentBody = `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`; + + if (resultExplanation) { + commentBody += `\n\nAdditional details about failure:\n${resultExplanation}`; + } + + github.rest.issues.createComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: commentBody + }); + process.env.AGENT_RESPONDED = 'true'; + } + + # Leave error comment when both PR/Issue comment handling fail + - name: Fallback Error Comment + uses: actions/github-script@v7 + if: ${{ env.AGENT_RESPONDED == 'false' }} # Only run if no conditions were met in previous steps + env: + ISSUE_NUMBER: ${{ env.ISSUE_NUMBER }} + with: + github-token: ${{ secrets.PAT_TOKEN || github.token }} + script: | + const issueNumber = process.env.ISSUE_NUMBER; + + github.rest.issues.createComment({ + issue_number: issueNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.` + }); diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 350200a..3efbe49 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -7,7 +7,7 @@ on: branches: [main] workflow_dispatch: schedule: - - cron: '0 2 * * 1' # Weekly on Monday at 2 AM UTC + - cron: "0 2 * * 1" # Weekly on Monday at 2 AM UTC jobs: # CodeQL Analysis @@ -18,25 +18,25 @@ jobs: actions: read contents: read security-events: write - + strategy: fail-fast: false matrix: - language: ['csharp'] - + language: ["csharp"] + steps: - name: πŸ“₯ Checkout uses: actions/checkout@v6 - + - name: πŸ” Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: security-extended,security-and-quality - + - name: πŸ” Autobuild uses: github/codeql-action/autobuild@v3 - + - name: πŸ” Perform Analysis uses: github/codeql-action/analyze@v3 with: @@ -46,65 +46,107 @@ jobs: snyk: name: πŸ›‘οΈ Snyk Security runs-on: ubuntu-latest - + steps: - name: πŸ“₯ Checkout uses: actions/checkout@v6 - + - name: πŸ—„οΈ Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: "8.0.x" - + - name: πŸ”§ Restore Dependencies run: dotnet restore MetarDecoder.sln --ignore-failed-sources - - - name: πŸ›‘οΈ Run Snyk - uses: snyk/actions/dotnet@master + + - name: πŸ›‘οΈ Install Snyk CLI + run: | + curl -s https://static.snyk.io/cli/latest/snyk-linux -o snyk + chmod +x ./snyk + sudo mv ./snyk /usr/local/bin/ + + - name: πŸ›‘οΈ Run Snyk Security Scan + run: | + snyk test --severity-threshold=high --all-projects env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --severity-threshold=high # SonarQube Analysis sonarqube: name: πŸ“Š SonarQube Analysis runs-on: ubuntu-latest if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' - + steps: - name: πŸ“₯ Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - + + - name: πŸ“¦ Setup NuGet + uses: NuGet/setup-nuget@v2.0.1 + - name: πŸ—„οΈ Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: "8.0.x" - - - name: πŸ“¦ Setup NuGet - uses: NuGet/setup-nuget@v2.0.1 - - - name: πŸ”§ Restore Dependencies - run: dotnet restore MetarDecoder.sln --ignore-failed-sources - - - name: πŸ—οΈ Build Solution - run: dotnet build MetarDecoder.sln --configuration Release --no-restore - - - name: πŸ“Š SonarQube Scan - uses: sonarsource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + - name: β˜• Set up JDK 17 + uses: actions/setup-java@v5 + with: + java-version: 17 + distribution: "zulu" + + - name: πŸ”§ Clear NuGet cache + run: dotnet nuget locals all --clear + + - name: πŸ“¦ Install SonarQube Tools + run: dotnet tool install --global --ignore-failed-sources dotnet-sonarscanner + + - name: πŸ“¦ Install Coverlet Tools + run: dotnet tool install --global --ignore-failed-sources coverlet.console + + - name: πŸ”§ Fix Permission + run: chmod 777 sonar/ -R || true + + - name: πŸ” Prepare analysis on SonarQube + run: | + echo "πŸ” Checking SonarQube configuration..." + if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then + echo "❌ SONAR_TOKEN is not set or empty" + echo "⚠️ Skipping SonarQube analysis" + exit 0 + fi + + echo "βœ… SONAR_TOKEN is configured" + dotnet sonarscanner begin \ + /o:"afonsoft" \ + /k:"afonsoft_metar-decoder" \ + /d:sonar.host.url="https://sonarcloud.io" \ + /d:sonar.login="${{ secrets.SONAR_TOKEN }}" \ + /d:sonar.scm.provider=git \ + /d:sonar.coverage.exclusions="**Test*.cs" + + - name: πŸ—οΈ Build + run: dotnet build MetarDecoder.sln --configuration release + + - name: πŸ” Run Code Analysis + run: | + echo "πŸ” Finalizing SonarQube analysis..." + if [ -z "${{ secrets.SONAR_TOKEN }}" ]; then + echo "⚠️ SONAR_TOKEN not configured, skipping analysis" + exit 0 + fi + + dotnet sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" # Security Summary security-summary: name: πŸ“‹ Security Summary runs-on: ubuntu-latest - needs: [codeql, snyk, sonarqube] + needs: [codeql, snyk] if: always() - + steps: - name: πŸ“‹ Generate Security Report run: | @@ -114,17 +156,17 @@ jobs: echo "|------|--------|" >> $GITHUB_STEP_SUMMARY echo "| πŸ” CodeQL | ${{ needs.codeql.result }} |" >> $GITHUB_STEP_SUMMARY echo "| πŸ›‘οΈ Snyk | ${{ needs.snyk.result }} |" >> $GITHUB_STEP_SUMMARY - echo "| πŸ“Š SonarQube | ${{ needs.sonarqube.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| πŸ“Š SonarQube | ${{ contains(needs.*.result, 'skipped') && 'skipped' || 'N/A' }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - if [[ "${{ needs.codeql.result }}" == "failure" || "${{ needs.snyk.result }}" == "failure" || "${{ needs.sonarqube.result }}" == "failure" ]]; then + + if [[ "${{ needs.codeql.result }}" == "failure" || "${{ needs.snyk.result }}" == "failure" ]]; then echo "❌ **Security issues detected! Please review the scan results.**" >> $GITHUB_STEP_SUMMARY else echo "βœ… **All security scans passed successfully!**" >> $GITHUB_STEP_SUMMARY fi - + - name: 🚨 Security Alert - if: needs.codeql.result == 'failure' || needs.snyk.result == 'failure' || needs.sonarqube.result == 'failure' + if: needs.codeql.result == 'failure' || needs.snyk.result == 'failure' run: | echo "🚨 SECURITY ISSUES DETECTED!" echo "Please review the security scan results immediately." diff --git a/src/Metar.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs b/src/Metar.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs index 816fc36..f2ff3b5 100644 --- a/src/Metar.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs +++ b/src/Metar.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs @@ -7,6 +7,7 @@ public sealed class DatetimeChunkDecoder : MetarChunkDecoder { public const string DayParameterName = "Day"; public const string TimeParameterName = "Time"; + public const string ObservationDateTimeParameterName = "ObservationDateTime"; public override string GetRegex() { @@ -39,6 +40,27 @@ public override Dictionary Parse(string remainingMetar, bool wit result.Add(DayParameterName, day); result.Add(TimeParameterName, $"{hour:00}:{minute:00} UTC"); + // Create DateTime from parsed components + var currentYear = DateTime.Now.Year; + var month = DateTime.Now.Month; + + // Handle day/year rollover - if day > current day, assume previous month + if (day > DateTime.Now.Day) + { + if (month == 1) + { + month = 12; + currentYear--; + } + else + { + month--; + } + } + + var observationDateTime = new DateTime(currentYear, month, day, hour, minute, 0, DateTimeKind.Utc); + result.Add(ObservationDateTimeParameterName, observationDateTime); + return GetResults(newRemainingMetar, result); } diff --git a/src/Metar.Decoder/Entity/DecodedMetar.cs b/src/Metar.Decoder/Entity/DecodedMetar.cs index 554d665..039fe02 100644 --- a/src/Metar.Decoder/Entity/DecodedMetar.cs +++ b/src/Metar.Decoder/Entity/DecodedMetar.cs @@ -1,4 +1,5 @@ -ο»Ώusing System.Collections.Generic; +ο»Ώusing System; +using System.Collections.Generic; using System.Collections.ObjectModel; namespace Metar.Decoder.Entity @@ -78,10 +79,15 @@ public ReadOnlyCollection DecodingExceptions public int? Day { get; set; } /// - /// Time of the observation, as a string + /// Time of observation, as a string /// public string Time { get; set; } = string.Empty; + /// + /// Date and time of observation + /// + public DateTime? ObservationDateTime { get; set; } + /// /// Report status (AUTO or NIL) /// diff --git a/src/Taf.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs b/src/Taf.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs index 456bbc8..cb93602 100644 --- a/src/Taf.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs +++ b/src/Taf.Decoder/ChunkDecoder/DatetimeChunkDecoder.cs @@ -7,6 +7,7 @@ public sealed class DatetimeChunkDecoder : TafChunkDecoder { public const string DayParameterName = "Day"; public const string TimeParameterName = "Time"; + public const string OriginDateTimeParameterName = "OriginDateTime"; public override string GetRegex() { @@ -38,6 +39,27 @@ public override Dictionary Parse(string remainingTaf, bool withC result.Add(DayParameterName, day); result.Add(TimeParameterName, $"{hour:00}:{minute:00} UTC"); + // Create DateTime from parsed components + var currentYear = DateTime.Now.Year; + var month = DateTime.Now.Month; + + // Handle day/year rollover - if day > current day, assume previous month + if (day > DateTime.Now.Day) + { + if (month == 1) + { + month = 12; + currentYear--; + } + else + { + month--; + } + } + + var originDateTime = new DateTime(currentYear, month, day, hour, minute, 0, DateTimeKind.Utc); + result.Add(OriginDateTimeParameterName, originDateTime); + return GetResults(newRemainingTaf, result); } diff --git a/src/Taf.Decoder/Entity/DecodedTaf.cs b/src/Taf.Decoder/Entity/DecodedTaf.cs index 81b1897..9fed6b9 100644 --- a/src/Taf.Decoder/Entity/DecodedTaf.cs +++ b/src/Taf.Decoder/Entity/DecodedTaf.cs @@ -1,4 +1,5 @@ -ο»Ώusing System.Collections.Generic; +ο»Ώusing System; +using System.Collections.Generic; using System.Collections.ObjectModel; namespace Taf.Decoder.entity @@ -69,6 +70,11 @@ public ReadOnlyCollection DecodingExceptions /// public string Time { get; set; } = string.Empty; + /// + /// Date and time of origin + /// + public DateTime? OriginDateTime { get; set; } + /// /// Forecast period ///