Ballerina Daily Build #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Ballerina Daily Build | |
| on: | |
| schedule: | |
| # 05:00 UTC = 10:30 Colombo (nominal). Actual firing drifts 0–30 min | |
| # later; build completes well under an hour, so the run finishes | |
| # comfortably before noon Colombo. Exact timing is not achievable | |
| # with scheduled triggers — platform limitation. | |
| - cron: '0 5 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| sendNotification: | |
| description: 'Send Google Chat notifications (success/failure)' | |
| type: boolean | |
| default: true | |
| jobs: | |
| PrepareBranches: | |
| name: Prepare branch matrix | |
| runs-on: ubuntu-latest | |
| outputs: | |
| vscodeBranches: ${{ steps.branches.outputs.vscodeBranches }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Load branch matrix config | |
| id: branches | |
| run: | | |
| set -euo pipefail | |
| CONFIG=.github/daily-build/release.properties | |
| if [ ! -f "$CONFIG" ]; then | |
| echo "::error::Missing $CONFIG" | |
| exit 1 | |
| fi | |
| branches=$(awk -F= ' | |
| /^[[:space:]]*#/ || /^[[:space:]]*$/ { next } | |
| { | |
| key=$1 | |
| gsub(/^[[:space:]]+|[[:space:]]+$/, "", key) | |
| if (key == "vscodeBranches") { | |
| value=substr($0, index($0, "=") + 1) | |
| gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) | |
| print value | |
| exit | |
| } | |
| } | |
| ' "$CONFIG") | |
| if [ -z "$branches" ]; then | |
| echo "::error::Required key 'vscodeBranches' missing or blank in $CONFIG" | |
| exit 1 | |
| fi | |
| json=$( | |
| IFS=',' read -ra branchArray <<< "$branches" | |
| for branch in "${branchArray[@]}"; do | |
| branch="${branch#"${branch%%[![:space:]]*}"}" | |
| branch="${branch%"${branch##*[![:space:]]}"}" | |
| if [ -z "$branch" ]; then | |
| echo "::error::vscodeBranches contains an empty branch entry" >&2 | |
| exit 1 | |
| fi | |
| printf '%s\n' "$branch" | |
| done | jq -R -s -c 'split("\n")[:-1]' | |
| ) | |
| echo "vscodeBranches=$json" >> "$GITHUB_OUTPUT" | |
| echo "Using Ballerina daily-build branches: $json" | |
| Build: | |
| name: Build Ballerina Extension (${{ matrix.vscodeBranch }}) | |
| needs: PrepareBranches | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| vscodeBranch: ${{ fromJson(needs.PrepareBranches.outputs.vscodeBranches) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ matrix.vscodeBranch }} | |
| - name: Slugify branch name | |
| id: slug | |
| run: | | |
| slug=$(echo "${{ matrix.vscodeBranch }}" | tr '/' '-') | |
| echo "slug=$slug" >> "$GITHUB_OUTPUT" | |
| - name: Load per-branch config | |
| id: config | |
| run: | | |
| set -euo pipefail | |
| CONFIG=.github/daily-build/release.properties | |
| if [ ! -f "$CONFIG" ]; then | |
| echo "::error::Missing $CONFIG on branch ${{ matrix.vscodeBranch }}" | |
| exit 1 | |
| fi | |
| declare -A parsed=() | |
| while IFS='=' read -r key value; do | |
| [[ -z "${key// /}" || "$key" =~ ^[[:space:]]*# ]] && continue | |
| key="${key// /}" | |
| value="${value## }"; value="${value%% }" | |
| parsed["$key"]="$value" | |
| echo "${key}=${value}" | |
| echo "${key}=${value}" >> "$GITHUB_OUTPUT" | |
| done < "$CONFIG" | |
| for required in lsArtifactPrefix productIntegratorBranch; do | |
| if [ -z "${parsed[$required]:-}" ]; then | |
| echo "::error::Required key '$required' missing or blank in $CONFIG on branch ${{ matrix.vscodeBranch }}" | |
| exit 1 | |
| fi | |
| done | |
| # LS artifact fetch runs BEFORE Setup Rush so LS lookup failures abort | |
| # the job in ~10s instead of wasting ~1-2 min on pnpm/node install. | |
| - name: Fetch latest LS daily-build artifact | |
| id: ls | |
| env: | |
| GH_TOKEN: ${{ secrets.CHOREO_BOT_TOKEN || secrets.GITHUB_TOKEN }} | |
| LS_REPO: ballerina-platform/ballerina-language-server | |
| LS_WORKFLOW: daily-build.yml | |
| ARTIFACT_PREFIX: ${{ steps.config.outputs.lsArtifactPrefix }} | |
| run: | | |
| set -euo pipefail | |
| api() { | |
| curl -fsSL \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "$@" | |
| } | |
| # We iterate recent runs (any conclusion) newest-first and pick the first | |
| # one that actually has an artifact matching our prefix. The LS workflow | |
| # publishes artifacts from its pack stage before running tests, so runs | |
| # can finish with conclusion=failure while still carrying a usable | |
| # artifact — we tolerate that on purpose. A run matching ARTIFACT_PREFIX | |
| # proves the pack stage for this branch succeeded (other branches' | |
| # failures publish different prefixes or none). | |
| echo "Looking up recent runs of $LS_WORKFLOW on $LS_REPO..." | |
| runs=$(api "https://api.github.com/repos/${LS_REPO}/actions/workflows/${LS_WORKFLOW}/runs?per_page=10") | |
| mapfile -t runEntries < <(echo "$runs" | jq -r '.workflow_runs[] | "\(.id) \(.conclusion // "in_progress") \(.created_at)"') | |
| if [ ${#runEntries[@]} -eq 0 ]; then | |
| echo "::error::No recent runs found for $LS_WORKFLOW on $LS_REPO" | |
| exit 1 | |
| fi | |
| selectedRunId="" | |
| selectedArtifact="" | |
| selectedRunConclusion="" | |
| selectedRunCreated="" | |
| for entry in "${runEntries[@]}"; do | |
| read -r rid rconc rat <<<"$entry" | |
| if [ "$rconc" = "in_progress" ] || [ "$rconc" = "null" ]; then | |
| echo "Skipping run $rid (still in progress, created=$rat)" | |
| continue | |
| fi | |
| arts=$(api "https://api.github.com/repos/${LS_REPO}/actions/runs/${rid}/artifacts?per_page=100") | |
| art=$(echo "$arts" | jq -r --arg p "$ARTIFACT_PREFIX" \ | |
| '[.artifacts[] | select(.name | startswith($p))][0]') | |
| if [ -n "$art" ] && [ "$art" != "null" ]; then | |
| selectedRunId="$rid" | |
| selectedArtifact="$art" | |
| selectedRunConclusion="$rconc" | |
| selectedRunCreated="$rat" | |
| echo "Selected run $rid (conclusion=$rconc, created=$rat) with prefix '$ARTIFACT_PREFIX'" | |
| break | |
| else | |
| echo "Run $rid (conclusion=$rconc) has no artifact matching '$ARTIFACT_PREFIX'; trying next" | |
| fi | |
| done | |
| if [ -z "$selectedRunId" ]; then | |
| echo "::error::No recent run of $LS_WORKFLOW has an artifact matching '$ARTIFACT_PREFIX'. Checked ${#runEntries[@]} runs." | |
| exit 1 | |
| fi | |
| if [ "$selectedRunConclusion" != "success" ]; then | |
| echo "::notice::Using LS artifact from run $selectedRunId despite run conclusion='$selectedRunConclusion' — pack stage succeeded and artifact is available." | |
| fi | |
| artifactId=$(echo "$selectedArtifact" | jq -r '.id') | |
| artifactName=$(echo "$selectedArtifact" | jq -r '.name') | |
| echo "Using artifact: $artifactName (id=$artifactId) from run $selectedRunId" | |
| # Parse LS version independently of ARTIFACT_PREFIX (the prefix is used | |
| # only for selection). Artifact name shape: | |
| # ballerina-language-server-<lsVer>-<YYYYMMDD>-<HHMMSS>.jar | |
| # e.g. ballerina-language-server-1.8.0.m3-20260421-150453.jar -> 1.8.0.m3 | |
| lsVersion=$(echo "$artifactName" | sed -E ' | |
| s/^ballerina-language-server-// | |
| s/\.jar$// | |
| s/-[0-9]{8}-[0-9]{6}$//') | |
| echo "Parsed LS version: $lsVersion" | |
| echo "Downloading artifact zip..." | |
| curl -fsSL \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -o /tmp/ls-artifact.zip \ | |
| "https://api.github.com/repos/${LS_REPO}/actions/artifacts/${artifactId}/zip" | |
| # The downloaded "zip" from /artifacts/{id}/zip may either wrap the JAR | |
| # file or be the JAR itself (JARs are zips). Detect which by peeking at | |
| # the top-level entries: a wrapper zip lists one `*.jar` entry; a raw | |
| # JAR lists class files / META-INF / etc. | |
| mkdir -p /tmp/ls-unpack | |
| mapfile -t topEntries < <(unzip -Z1 /tmp/ls-artifact.zip | awk -F/ '{print $1}' | sort -u) | |
| innerJar=$(unzip -Z1 /tmp/ls-artifact.zip | grep -E '^[^/]+\.jar$' | head -1 || true) | |
| # Preserve the artifact's full timestamped filename so the JAR shipped | |
| # in the VSIX traces back to the exact LS build. | |
| target="workspaces/ballerina/ballerina-extension/ls/${artifactName}" | |
| mkdir -p workspaces/ballerina/ballerina-extension/ls | |
| rm -f workspaces/ballerina/ballerina-extension/ls/*.jar | |
| if [ -n "$innerJar" ]; then | |
| echo "Artifact is a wrapper zip; extracting $innerJar" | |
| unzip -o /tmp/ls-artifact.zip -d /tmp/ls-unpack >/dev/null | |
| cp "/tmp/ls-unpack/$innerJar" "$target" | |
| else | |
| echo "Artifact is the JAR itself (no inner .jar entry); copying directly" | |
| cp /tmp/ls-artifact.zip "$target" | |
| fi | |
| ls -la workspaces/ballerina/ballerina-extension/ls/ | |
| echo "lsVersion=$lsVersion" >> "$GITHUB_OUTPUT" | |
| echo "lsArtifactName=$artifactName" >> "$GITHUB_OUTPUT" | |
| echo "lsRunId=$selectedRunId" >> "$GITHUB_OUTPUT" | |
| echo "lsRunConclusion=$selectedRunConclusion" >> "$GITHUB_OUTPUT" | |
| - name: Setup Rush | |
| uses: gigara/setup-rush@v1.2.0 | |
| with: | |
| pnpm: 10.10.0 | |
| node: 22.x | |
| cache-rush: true | |
| cache-pnpm: true | |
| - name: Rush install | |
| run: node common/scripts/install-run-rush.js install | |
| - name: Compute version | |
| id: version | |
| run: | | |
| set -euo pipefail | |
| currentVersion=$(node -p "require('./workspaces/ballerina/ballerina-extension/package.json').version") | |
| base=$(echo "$currentVersion" | sed -E 's/[-+].*$//') | |
| datePart=$(date -u '+%y%m%d') | |
| timePart=$(date -u '+%H%M') | |
| newVersion="${base}-${datePart}-${timePart}" | |
| echo "version=$newVersion" >> "$GITHUB_OUTPUT" | |
| echo "Computed version: $newVersion" | |
| - name: Update extension version | |
| run: | | |
| cd workspaces/ballerina/ballerina-extension | |
| npm version ${{ steps.version.outputs.version }} --no-git-tag-version | |
| - name: Copy .env.example to .env | |
| run: | | |
| find . -type f -name ".env.example" | while read example; do | |
| envfile="$(dirname "$example")/.env" | |
| cp "$example" "$envfile" | |
| done | |
| ballerinaEnv="workspaces/ballerina/ballerina-extension/.env" | |
| for key in COPILOT_ROOT_URL COPILOT_DEV_ROOT_URL APPINSIGHTS_INSTRUMENTATION_KEY; do | |
| if grep -q "^${key}=" "$ballerinaEnv"; then | |
| sed -i "s|^${key}=.*|${key}=|" "$ballerinaEnv" | |
| else | |
| echo "${key}=" >> "$ballerinaEnv" | |
| fi | |
| done | |
| - name: Build Ballerina extension | |
| run: node common/scripts/install-run-rush.js build -t ballerina --verbose | |
| env: | |
| isPreRelease: true | |
| COPILOT_ROOT_URL: ${{ secrets.COPILOT_ROOT_URL }} | |
| COPILOT_DEV_ROOT_URL: ${{ secrets.COPILOT_DEV_ROOT_URL }} | |
| APPINSIGHTS_INSTRUMENTATION_KEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATION_KEY }} | |
| - name: Run Ballerina package tests | |
| continue-on-error: true | |
| env: | |
| PACKAGES: ballerina-visualizer ballerina-side-panel type-diagram sequence-diagram component-diagram bi-diagram | |
| run: | | |
| set +e | |
| mkdir -p /tmp/snapshot | |
| echo "$PACKAGES" > /tmp/snapshot/.packages | |
| for pkg in $PACKAGES; do | |
| echo "::group::Package tests — $pkg" | |
| (cd "workspaces/ballerina/$pkg" && pnpm test) 2>&1 | tee "/tmp/snapshot/${pkg}.log" | |
| rc=${PIPESTATUS[0]} | |
| if [ "$rc" -eq 0 ]; then | |
| echo pass > "/tmp/snapshot/${pkg}.status" | |
| else | |
| echo fail > "/tmp/snapshot/${pkg}.status" | |
| echo "::warning::Package tests failed for $pkg (non-blocking for daily build)" | |
| fi | |
| echo "::endgroup::" | |
| done | |
| exit 0 | |
| - name: Prepare Trivy output directory | |
| run: mkdir -p /tmp/trivy | |
| - name: Run Trivy vulnerability scanner | |
| id: trivy | |
| continue-on-error: true | |
| uses: aquasecurity/trivy-action@0.35.0 | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'table' | |
| output: '/tmp/trivy/trivy-results.txt' | |
| exit-code: '1' | |
| timeout: '10m' | |
| skip-dirs: 'common/temp' | |
| ignore-unfixed: true | |
| - name: Get VSIX file name | |
| id: vsix | |
| run: | | |
| shopt -s nullglob | |
| files=(ballerina-*.vsix) | |
| if [ ${#files[@]} -eq 0 ]; then | |
| echo "VSIX not found at root, copying manually..." | |
| cp workspaces/ballerina/ballerina-extension/vsix/*.vsix ./ 2>/dev/null || true | |
| files=(ballerina-*.vsix) | |
| fi | |
| if [ ${#files[@]} -ne 1 ]; then | |
| echo "Expected exactly one Ballerina VSIX, found ${#files[@]}:" | |
| printf ' - %s\n' "${files[@]}" | |
| exit 1 | |
| fi | |
| file="${files[0]}" | |
| echo "fileName=$file" >> "$GITHUB_OUTPUT" | |
| echo "Built VSIX: $file" | |
| - name: Write job summary | |
| run: | | |
| set -euo pipefail | |
| branch='${{ matrix.vscodeBranch }}' | |
| version='${{ steps.version.outputs.version }}' | |
| vsix='${{ steps.vsix.outputs.fileName }}' | |
| lsArtifact='${{ steps.ls.outputs.lsArtifactName }}' | |
| lsRunId='${{ steps.ls.outputs.lsRunId }}' | |
| lsRunUrl="https://github.com/ballerina-platform/ballerina-language-server/actions/runs/${lsRunId}" | |
| { | |
| echo "#### Daily build — ${branch}" | |
| echo "" | |
| echo "| Field | Value |" | |
| echo "|---|---|" | |
| echo "| Version | \`${version}\` |" | |
| echo "| VSIX | \`${vsix}\` |" | |
| echo "| LS JAR | \`${lsArtifact}\` |" | |
| echo "| LS source run | [${lsRunId}](${lsRunUrl}) |" | |
| echo "" | |
| echo "#### Package tests" | |
| echo "" | |
| echo "| Package | Test Suites | Tests | Status |" | |
| echo "|---|---|---|---|" | |
| for pkg in $(cat /tmp/snapshot/.packages); do | |
| log="/tmp/snapshot/${pkg}.log" | |
| status=$(cat "/tmp/snapshot/${pkg}.status" 2>/dev/null || echo "fail") | |
| suites="—" | |
| tests="—" | |
| if [ -f "$log" ]; then | |
| s=$(grep -E '^Test Suites:' "$log" | tail -1 | sed 's/^Test Suites:[[:space:]]*//' || true) | |
| t=$(grep -E '^Tests:' "$log" | tail -1 | sed 's/^Tests:[[:space:]]*//' || true) | |
| [ -n "${s:-}" ] && suites="$s" | |
| [ -n "${t:-}" ] && tests="$t" | |
| fi | |
| if [ "$status" = "pass" ]; then | |
| status_txt="passed" | |
| else | |
| status_txt="failed" | |
| fi | |
| echo "| ${pkg} | ${suites} | ${tests} | ${status_txt} |" | |
| done | |
| echo "" | |
| echo "#### Trivy scan" | |
| echo "" | |
| if [ '${{ steps.trivy.outcome }}' = "success" ]; then | |
| echo "Status: passed" | |
| else | |
| echo "Status: failed (non-blocking)" | |
| fi | |
| echo "" | |
| if [ -s /tmp/trivy/trivy-results.txt ]; then | |
| echo "<details><summary>Trivy output excerpt</summary>" | |
| echo "" | |
| echo '```' | |
| sed -n '1,120p' /tmp/trivy/trivy-results.txt | |
| echo '```' | |
| echo "" | |
| echo "</details>" | |
| else | |
| echo "No Trivy output file was produced." | |
| fi | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload VSIX artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ballerina-daily-vsix-${{ steps.slug.outputs.slug }} | |
| path: ${{ steps.vsix.outputs.fileName }} | |
| retention-days: 30 | |
| # Per-branch metadata artifact. Matrix-aggregated `needs.Build.outputs.*` is last-writer-wins | |
| # in GitHub Actions, so downstream matrix legs cannot rely on it for per-leg values. Each | |
| # leg uploads its own metadata file keyed by branch slug; downstream legs download the | |
| # matching one. | |
| - name: Write build metadata | |
| run: | | |
| mkdir -p build-metadata | |
| cat > build-metadata/build.env <<EOF | |
| version=${{ steps.version.outputs.version }} | |
| vsixFileName=${{ steps.vsix.outputs.fileName }} | |
| lsVersion=${{ steps.ls.outputs.lsVersion }} | |
| lsArtifactName=${{ steps.ls.outputs.lsArtifactName }} | |
| lsRunId=${{ steps.ls.outputs.lsRunId }} | |
| lsRunConclusion=${{ steps.ls.outputs.lsRunConclusion }} | |
| productIntegratorBranch=${{ steps.config.outputs.productIntegratorBranch }} | |
| vscodeBranch=${{ matrix.vscodeBranch }} | |
| branchSlug=${{ steps.slug.outputs.slug }} | |
| EOF | |
| - name: Upload build metadata artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ballerina-daily-metadata-${{ steps.slug.outputs.slug }} | |
| path: build-metadata/build.env | |
| retention-days: 30 | |
| Release: | |
| name: Release (${{ matrix.vscodeBranch }}) | |
| needs: [PrepareBranches, Build] | |
| # !cancelled() lets this job run even when a different matrix leg of Build failed — | |
| # per-leg isolation. The `gate` step below skips this specific leg if its own | |
| # Build didn't produce the metadata artifact. | |
| if: ${{ !cancelled() && github.repository == 'wso2/vscode-extensions' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| vscodeBranch: ${{ fromJson(needs.PrepareBranches.outputs.vscodeBranches) }} | |
| steps: | |
| - name: Slugify branch name | |
| id: slug | |
| run: echo "slug=$(echo '${{ matrix.vscodeBranch }}' | tr '/' '-')" >> "$GITHUB_OUTPUT" | |
| - name: Gate on this branch's Build success | |
| id: gate | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| artifact="ballerina-daily-metadata-${{ steps.slug.outputs.slug }}" | |
| found=$(gh api --paginate \ | |
| "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \ | |
| --jq ".artifacts[] | select(.name == \"$artifact\") | .id" | head -n1) | |
| if [ -z "$found" ]; then | |
| echo "Build for ${{ matrix.vscodeBranch }} did not produce $artifact; skipping this leg." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download VSIX artifact | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ballerina-daily-vsix-${{ steps.slug.outputs.slug }} | |
| - name: Download build metadata artifact | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ballerina-daily-metadata-${{ steps.slug.outputs.slug }} | |
| path: build-metadata | |
| - name: Load build metadata | |
| if: steps.gate.outputs.skip != 'true' | |
| id: meta | |
| run: | | |
| set -euo pipefail | |
| declare -A parsed=() | |
| while IFS='=' read -r key value; do | |
| [[ -z "${key// /}" || "$key" =~ ^[[:space:]]*# ]] && continue | |
| parsed["$key"]="$value" | |
| echo "${key}=${value}" >> "$GITHUB_OUTPUT" | |
| done < build-metadata/build.env | |
| for required in version lsVersion vsixFileName; do | |
| if [ -z "${parsed[$required]:-}" ]; then | |
| echo "::error::Required key '$required' missing or blank in build-metadata/build.env for branch ${{ matrix.vscodeBranch }}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Create release on wso2/ballerina-vscode | |
| if: steps.gate.outputs.skip != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.CHOREO_BOT_TOKEN }} | |
| NEW_VERSION: ${{ steps.meta.outputs.version }} | |
| LS_ARTIFACT_NAME: ${{ steps.meta.outputs.lsArtifactName }} | |
| LS_RUN_ID: ${{ steps.meta.outputs.lsRunId }} | |
| SOURCE_BRANCH: ${{ matrix.vscodeBranch }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| set -euo pipefail | |
| TAG="v${NEW_VERSION}" | |
| RELEASE_NAME="Ballerina Extension Daily Build v${NEW_VERSION}" | |
| shopt -s nullglob | |
| files=(ballerina-*.vsix) | |
| if [ ${#files[@]} -ne 1 ]; then | |
| echo "::error::Expected exactly one VSIX file, found ${#files[@]}" | |
| exit 1 | |
| fi | |
| VSIX_FILE="${files[0]}" | |
| # Display the full snapshot name (with timestamp) rather than the | |
| # stripped version — traceability to the exact LS build. | |
| LS_SNAPSHOT="${LS_ARTIFACT_NAME#ballerina-language-server-}" | |
| LS_SNAPSHOT="${LS_SNAPSHOT%.jar}" | |
| LS_RUN_URL="https://github.com/ballerina-platform/ballerina-language-server/actions/runs/${LS_RUN_ID}" | |
| body=$(jq -n \ | |
| --arg branch "$SOURCE_BRANCH" \ | |
| --arg ls "$LS_SNAPSHOT" \ | |
| --arg lsRun "$LS_RUN_URL" \ | |
| --arg run "$RUN_URL" \ | |
| '"Automated daily build.\n\n- Source branch: `\($branch)`\n- Ballerina language server: `\($ls)`\n- LS run: \($lsRun)\n- Workflow run: \($run)"') | |
| payload=$(jq -n \ | |
| --arg tag "$TAG" \ | |
| --arg name "$RELEASE_NAME" \ | |
| --argjson body "$body" \ | |
| '{tag_name:$tag, name:$name, draft:false, prerelease:true, body:$body}') | |
| createResponse=$(curl -sS -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| -d "$payload" \ | |
| "https://api.github.com/repos/wso2/ballerina-vscode/releases") | |
| releaseId=$(echo "$createResponse" | jq -r '.id // empty') | |
| if [ -z "$releaseId" ] || [ "$releaseId" = "null" ]; then | |
| echo "::error::Failed to create release" | |
| echo "$createResponse" | |
| exit 1 | |
| fi | |
| echo "Created release ID: $releaseId" | |
| uploadResponse=$(curl -sS -w "\n%{http_code}" -X POST \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Content-Type: application/octet-stream" \ | |
| --data-binary @"$VSIX_FILE" \ | |
| "https://uploads.github.com/repos/wso2/ballerina-vscode/releases/${releaseId}/assets?name=${VSIX_FILE}") | |
| httpCode=$(echo "$uploadResponse" | tail -1) | |
| responseBody=$(echo "$uploadResponse" | sed '$d') | |
| if [ "$httpCode" -ge 200 ] && [ "$httpCode" -lt 300 ]; then | |
| echo "Upload successful: $(echo "$responseBody" | jq -r '.name')" | |
| else | |
| echo "::error::Upload failed with HTTP $httpCode" | |
| echo "$responseBody" | |
| exit 1 | |
| fi | |
| # Per-leg release-success marker. Downstream gates key off this — the | |
| # metadata artifact alone isn't enough (Build can succeed while Release | |
| # fails, e.g. release creation or asset upload errors). | |
| - name: Mark release success | |
| if: steps.gate.outputs.skip != 'true' | |
| run: | | |
| mkdir -p release-ok | |
| echo "releaseId=required" > release-ok/release.env | |
| - name: Upload release success marker | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ballerina-daily-release-ok-${{ steps.slug.outputs.slug }} | |
| path: release-ok/release.env | |
| retention-days: 30 | |
| UpdateProductIntegrator: | |
| name: Update product-integrator (${{ matrix.vscodeBranch }}) | |
| needs: [PrepareBranches, Build, Release] | |
| # Per-leg gating: !cancelled() allows this leg to run even when another matrix | |
| # leg of Build/Release failed. The `gate` step skips this specific leg if its | |
| # own Release didn't complete successfully (marker artifact absent). | |
| if: ${{ !cancelled() && github.repository == 'wso2/vscode-extensions' }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| vscodeBranch: ${{ fromJson(needs.PrepareBranches.outputs.vscodeBranches) }} | |
| steps: | |
| - name: Slugify branch name | |
| id: slug | |
| run: echo "slug=$(echo '${{ matrix.vscodeBranch }}' | tr '/' '-')" >> "$GITHUB_OUTPUT" | |
| - name: Gate on this branch's Release success | |
| id: gate | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Key off the release-ok marker rather than the build metadata: Build | |
| # can succeed while Release fails (e.g. GH API hiccup during release | |
| # create or asset upload). If we gate on metadata alone, we would | |
| # open a product-integrator PR referencing a release that doesn't | |
| # exist. The marker is only uploaded at the end of a successful | |
| # Release run for this specific matrix leg. | |
| artifact="ballerina-daily-release-ok-${{ steps.slug.outputs.slug }}" | |
| found=$(gh api --paginate \ | |
| "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \ | |
| --jq ".artifacts[] | select(.name == \"$artifact\") | .id" | head -n1) | |
| if [ -z "$found" ]; then | |
| echo "Release for ${{ matrix.vscodeBranch }} did not produce $artifact; skipping this leg." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Download build metadata artifact | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ballerina-daily-metadata-${{ steps.slug.outputs.slug }} | |
| path: build-metadata | |
| - name: Load build metadata | |
| if: steps.gate.outputs.skip != 'true' | |
| id: meta | |
| run: | | |
| set -euo pipefail | |
| declare -A parsed=() | |
| while IFS='=' read -r key value; do | |
| [[ -z "${key// /}" || "$key" =~ ^[[:space:]]*# ]] && continue | |
| parsed["$key"]="$value" | |
| echo "${key}=${value}" >> "$GITHUB_OUTPUT" | |
| done < build-metadata/build.env | |
| for required in version productIntegratorBranch; do | |
| if [ -z "${parsed[$required]:-}" ]; then | |
| echo "::error::Required key '$required' missing or blank in build-metadata/build.env for branch ${{ matrix.vscodeBranch }}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Checkout product-integrator | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: wso2/product-integrator | |
| ref: ${{ steps.meta.outputs.productIntegratorBranch }} | |
| token: ${{ secrets.CHOREO_BOT_TOKEN }} | |
| path: product-integrator | |
| - name: Update ballerina.extension.version and open PR | |
| id: productIntegratorPr | |
| if: steps.gate.outputs.skip != 'true' | |
| working-directory: product-integrator | |
| env: | |
| NEW_VERSION: ${{ steps.meta.outputs.version }} | |
| GH_TOKEN: ${{ secrets.CHOREO_BOT_TOKEN }} | |
| BASE_BRANCH: ${{ steps.meta.outputs.productIntegratorBranch }} | |
| SOURCE_BRANCH: ${{ matrix.vscodeBranch }} | |
| run: | | |
| set -euo pipefail | |
| PROPERTIES_FILE="ci/build/component-versions.properties" | |
| slug=$(echo "$SOURCE_BRANCH" | tr '/' '-') | |
| BRANCH_NAME="update-bal-ext-version-${slug}-${NEW_VERSION}" | |
| sed -i "s/^ballerina\.extension\.version=.*/ballerina.extension.version=${NEW_VERSION}/" "$PROPERTIES_FILE" | |
| grep "ballerina.extension.version" "$PROPERTIES_FILE" | |
| if git diff --quiet; then | |
| echo "No changes detected, skipping PR creation" | |
| exit 0 | |
| fi | |
| git config user.name "WSO2 Builder" | |
| git config user.email "builder@wso2.com" | |
| git checkout -b "$BRANCH_NAME" | |
| git add "$PROPERTIES_FILE" | |
| git commit -m "Update ballerina.extension.version to ${NEW_VERSION}" | |
| git push origin "$BRANCH_NAME" | |
| pr_body=$(printf 'Automated PR to update Ballerina extension version after daily build.\n\n- Source vscode branch: %s\n- Release: https://github.com/wso2/ballerina-vscode/releases/tag/v%s' "$SOURCE_BRANCH" "$NEW_VERSION") | |
| payload=$(jq -n \ | |
| --arg title "Update ballerina.extension.version to ${NEW_VERSION}" \ | |
| --arg head "$BRANCH_NAME" \ | |
| --arg base "$BASE_BRANCH" \ | |
| --arg body "$pr_body" \ | |
| '{title:$title, head:$head, base:$base, body:$body}') | |
| pr_response=$(curl -sS -X POST \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -d "$payload" \ | |
| "https://api.github.com/repos/wso2/product-integrator/pulls") | |
| pr_url=$(echo "$pr_response" | jq -r '.html_url // empty') | |
| if [ -n "$pr_url" ] && [ "$pr_url" != "null" ]; then | |
| echo "Created PR: $pr_url" | |
| echo "prUrl=$pr_url" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::error::Failed to create PR" | |
| echo "$pr_response" | |
| exit 1 | |
| fi | |
| # Per-leg product-integrator success marker. NotifySuccess gates on this | |
| # so a leg whose UpdateProductIntegrator failed does not get a | |
| # "daily build succeeded" chat notification alongside NotifyFailure. | |
| - name: Mark product-integrator success | |
| if: steps.gate.outputs.skip != 'true' | |
| run: | | |
| mkdir -p pi-ok | |
| { | |
| echo "updated=required" | |
| echo "prUrl=${{ steps.productIntegratorPr.outputs.prUrl }}" | |
| } > pi-ok/pi.env | |
| - name: Upload product-integrator success marker | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ballerina-daily-product-integrator-ok-${{ steps.slug.outputs.slug }} | |
| path: pi-ok/pi.env | |
| retention-days: 30 | |
| NotifySuccess: | |
| name: Notify Success (${{ matrix.vscodeBranch }}) | |
| needs: [PrepareBranches, Build, Release, UpdateProductIntegrator] | |
| # Per-leg gating: !cancelled() allows this leg to run even when another matrix | |
| # leg failed. The `gate` step skips this specific leg if its own Build didn't | |
| # produce the metadata artifact. Manual dispatch can opt out via | |
| # sendNotification=false; scheduled runs always notify. | |
| if: ${{ !cancelled() && github.repository == 'wso2/vscode-extensions' && (github.event_name != 'workflow_dispatch' || inputs.sendNotification) }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| vscodeBranch: ${{ fromJson(needs.PrepareBranches.outputs.vscodeBranches) }} | |
| steps: | |
| - name: Slugify branch name | |
| id: slug | |
| run: echo "slug=$(echo '${{ matrix.vscodeBranch }}' | tr '/' '-')" >> "$GITHUB_OUTPUT" | |
| - name: Gate on this branch's Release and UpdateProductIntegrator success | |
| id: gate | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Only post success notification if BOTH Release and | |
| # UpdateProductIntegrator succeeded for this leg. `needs.*.result` | |
| # is aggregated across the matrix and can't be used for per-leg | |
| # gating, so we key off the two markers. A failure in either will | |
| # be surfaced by NotifyFailure. | |
| slug="${{ steps.slug.outputs.slug }}" | |
| releaseMarker="ballerina-daily-release-ok-${slug}" | |
| piMarker="ballerina-daily-product-integrator-ok-${slug}" | |
| artifacts=$(gh api --paginate \ | |
| "/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \ | |
| --jq '.artifacts[].name') | |
| missing="" | |
| for name in "$releaseMarker" "$piMarker"; do | |
| if ! echo "$artifacts" | grep -Fxq "$name"; then | |
| missing="$missing $name" | |
| fi | |
| done | |
| if [ -n "$missing" ]; then | |
| echo "Missing markers for ${{ matrix.vscodeBranch }}:$missing; skipping notify." | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Checkout without ref so the composite action at | |
| # ./.github/actions/dailyBuildNotification resolves to the workflow's | |
| # own commit (github.sha), not the checked-out leg's branch tip. This | |
| # keeps both matrix legs using the same action definition — otherwise | |
| # release/bi-1.8.x could load a stale copy of the action. | |
| - uses: actions/checkout@v4 | |
| if: steps.gate.outputs.skip != 'true' | |
| - name: Download VSIX artifact | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ballerina-daily-vsix-${{ steps.slug.outputs.slug }} | |
| - name: Download product-integrator metadata | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: ballerina-daily-product-integrator-ok-${{ steps.slug.outputs.slug }} | |
| path: pi-ok | |
| - name: Load product-integrator metadata | |
| if: steps.gate.outputs.skip != 'true' | |
| id: pi | |
| run: | | |
| set -euo pipefail | |
| prUrl="" | |
| if [ -f pi-ok/pi.env ]; then | |
| while IFS='=' read -r key value; do | |
| [[ -z "${key// /}" || "$key" =~ ^[[:space:]]*# ]] && continue | |
| if [ "$key" = "prUrl" ]; then | |
| prUrl="$value" | |
| fi | |
| done < pi-ok/pi.env | |
| fi | |
| echo "prUrl=$prUrl" >> "$GITHUB_OUTPUT" | |
| - name: Notification - Ballerina | |
| if: steps.gate.outputs.skip != 'true' | |
| uses: ./.github/actions/dailyBuildNotification | |
| with: | |
| title: "Ballerina (${{ matrix.vscodeBranch }})" | |
| fileName: ballerina | |
| chatAPI: ${{ secrets.BI_TEAM_CHAT_API }} | |
| prUrl: ${{ steps.pi.outputs.prUrl }} | |
| NotifyFailure: | |
| name: Notify Failure | |
| needs: [PrepareBranches, Build, Release, UpdateProductIntegrator] | |
| if: ${{ always() && contains(needs.*.result, 'failure') && github.repository == 'wso2/vscode-extensions' && (github.event_name != 'workflow_dispatch' || inputs.sendNotification) }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Failure Notification | |
| uses: ./.github/actions/failure-notification | |
| with: | |
| title: "Ballerina Daily Build Failed" | |
| run_id: ${{ github.run_id }} | |
| chat_api: ${{ secrets.TOOLING_TEAM_CHAT_API }} | |
| repository: ${{ github.repository }} |