Install smoke test #4
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: Install smoke test | |
| # Verifies that each install channel produces a working `plumb` binary | |
| # across platforms. Runs weekly and on every release tag. | |
| # | |
| # Matrix: {cargo, brew, npm, curl} x {ubuntu, macos, windows}. | |
| # All four install channels (cargo, brew, npm, curl) run live against | |
| # the latest published tag. | |
| # See dist-workspace.toml and docs/src/ci/release-prep.md for details. | |
| # | |
| # Non-gated failures open or update a single tracking issue so the team | |
| # gets notified without duplicate noise. | |
| on: | |
| schedule: | |
| - cron: "15 4 * * 1" # Weekly, Monday 04:15 UTC | |
| push: | |
| tags: | |
| - "v[0-9]+.[0-9]+.[0-9]+" | |
| - "v[0-9]+.[0-9]+.[0-9]+-*" | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Release tag to smoke (e.g. v0.0.11). Leave empty to use the latest release." | |
| required: false | |
| type: string | |
| default: "" | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: install-smoke-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| resolve-tag: | |
| name: Resolve release tag | |
| runs-on: ubuntu-latest | |
| outputs: | |
| tag: ${{ steps.tag.outputs.tag }} | |
| version: ${{ steps.tag.outputs.version }} | |
| steps: | |
| - name: Determine tag | |
| id: tag | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REPO: ${{ github.repository }} | |
| INPUT_TAG: ${{ inputs.tag }} | |
| run: | | |
| if [[ -n "$INPUT_TAG" ]]; then | |
| tag="$INPUT_TAG" | |
| elif [[ "$REF_TYPE" == "tag" ]]; then | |
| tag="$REF_NAME" | |
| else | |
| tag="$(gh release view --repo "$REPO" --json tagName -q .tagName 2>/dev/null || true)" | |
| fi | |
| if [[ -z "$tag" ]]; then | |
| echo "::warning::No release tag found — skipping smoke tests." | |
| echo "tag=" >> "$GITHUB_OUTPUT" | |
| echo "version=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "tag=$tag" >> "$GITHUB_OUTPUT" | |
| echo "version=${tag#v}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved tag: $tag" | |
| smoke: | |
| name: "${{ matrix.channel }} / ${{ matrix.os }}" | |
| needs: resolve-tag | |
| if: needs.resolve-tag.outputs.tag != '' | |
| runs-on: ${{ matrix.os }} | |
| continue-on-error: ${{ matrix.gated }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ── cargo ─────────────────────────────────────────────── | |
| - channel: cargo | |
| os: ubuntu-latest | |
| gated: false | |
| - channel: cargo | |
| os: macos-latest | |
| gated: false | |
| - channel: cargo | |
| os: windows-latest | |
| gated: false | |
| # ── curl (install script) ─────────────────────────────── | |
| - channel: curl | |
| os: ubuntu-latest | |
| gated: false | |
| - channel: curl | |
| os: macos-latest | |
| gated: false | |
| - channel: curl | |
| os: windows-latest | |
| gated: false | |
| # ── brew (live tap aram-devdocs/homebrew-plumb) ───────── | |
| - channel: brew | |
| os: ubuntu-latest | |
| gated: false | |
| - channel: brew | |
| os: macos-latest | |
| gated: false | |
| # ── npm (unscoped `plumb-cli`) ────────────────────────── | |
| - channel: npm | |
| os: ubuntu-latest | |
| gated: false | |
| - channel: npm | |
| os: macos-latest | |
| gated: false | |
| - channel: npm | |
| os: windows-latest | |
| gated: false | |
| steps: | |
| - name: Skip gated channel | |
| if: matrix.gated | |
| env: | |
| CHANNEL: ${{ matrix.channel }} | |
| run: | | |
| echo "::notice::$CHANNEL is gated — external prerequisites not yet available. Skipping." | |
| exit 0 | |
| # ── cargo install ───────────────────────────────────────── | |
| - name: Install via cargo | |
| if: "!matrix.gated && matrix.channel == 'cargo'" | |
| timeout-minutes: 45 | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.resolve-tag.outputs.version }} | |
| run: | | |
| cargo install plumb-cli --version "$VERSION" --locked | |
| # ── curl / install script ───────────────────────────────── | |
| - name: Install via curl (unix) | |
| if: "!matrix.gated && matrix.channel == 'curl' && runner.os != 'Windows'" | |
| shell: bash | |
| env: | |
| INSTALLER_URL: "https://github.com/${{ github.repository }}/releases/download/${{ needs.resolve-tag.outputs.tag }}/plumb-cli-installer.sh" | |
| REPO: ${{ github.repository }} | |
| run: | | |
| curl -LsSf -o plumb-cli-installer.sh "$INSTALLER_URL" | |
| gh attestation verify plumb-cli-installer.sh --repo "$REPO" | |
| sh plumb-cli-installer.sh | |
| - name: Install via curl (windows) | |
| if: "!matrix.gated && matrix.channel == 'curl' && runner.os == 'Windows'" | |
| shell: pwsh | |
| env: | |
| INSTALLER_URL: "https://github.com/${{ github.repository }}/releases/download/${{ needs.resolve-tag.outputs.tag }}/plumb-cli-installer.ps1" | |
| REPO: ${{ github.repository }} | |
| run: | | |
| $installer = Join-Path $PWD "plumb-cli-installer.ps1" | |
| Invoke-WebRequest -Uri "$env:INSTALLER_URL" -OutFile $installer | |
| gh attestation verify $installer --repo "$env:REPO" | |
| & $installer | |
| # ── brew ────────────────────────────────────────────────── | |
| - name: Install via brew | |
| if: "!matrix.gated && matrix.channel == 'brew'" | |
| shell: bash | |
| run: | | |
| brew install aram-devdocs/plumb/plumb | |
| # ── npm ─────────────────────────────────────────────────── | |
| - name: Install via npm | |
| if: "!matrix.gated && matrix.channel == 'npm'" | |
| shell: bash | |
| run: | | |
| npm install -g plumb-cli | |
| # ── Verify ──────────────────────────────────────────────── | |
| - name: Verify plumb | |
| if: "!matrix.gated" | |
| shell: bash | |
| run: | | |
| # On Windows the installer may update PATH outside bash's view. | |
| # Refresh and show diagnostics so PATH failures are explicit. | |
| if [[ "$RUNNER_OS" == "Windows" ]]; then | |
| export PATH="$USERPROFILE/.cargo/bin:$LOCALAPPDATA/Programs/plumb/bin:$PATH" | |
| echo "--- PATH diagnostic ---" | |
| which plumb || { echo "::error::plumb not found on PATH"; where plumb 2>/dev/null || true; echo "PATH=$PATH"; exit 1; } | |
| fi | |
| echo "--- plumb --version ---" | |
| plumb --version | |
| echo "--- plumb lint plumb-fake://hello ---" | |
| rc=0; plumb lint plumb-fake://hello || rc=$? | |
| if [[ "$rc" -ne 0 && "$rc" -ne 3 ]]; then | |
| echo "::error::plumb lint exited with unexpected code $rc" | |
| exit 1 | |
| fi | |
| report: | |
| name: Report failures | |
| needs: [resolve-tag, smoke] | |
| if: always() && needs.resolve-tag.outputs.tag != '' && needs.smoke.result == 'failure' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPORT_REPO: ${{ github.repository }} | |
| REPORT_TAG: ${{ needs.resolve-tag.outputs.tag }} | |
| REPORT_EVENT_NAME: ${{ github.event_name }} | |
| REPORT_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v6 | |
| - name: Assert shell runs use env indirection | |
| shell: bash | |
| run: | | |
| python3 - <<'PY' | |
| from pathlib import Path | |
| import sys | |
| workflow_path = Path(".github/workflows/install-smoke.yml") | |
| lines = workflow_path.read_text().splitlines() | |
| # This is a small stdlib guard for this workflow, not a general | |
| # YAML parser. It intentionally fail-closes on syntax it does not | |
| # recognize instead of trying to accept every GitHub Actions form. | |
| expr_open = "$" + "{{" | |
| # ubuntu-latest currently provides Python 3.10+, so `str | None` | |
| # is valid here without importing Optional. | |
| supported_run_markers = {"|", "|-", "|+"} | |
| in_jobs = False | |
| current_job = None | |
| in_steps = False | |
| current_step = None | |
| in_run = False | |
| run_indent = None | |
| bad_lines = [] | |
| unsupported = [] | |
| # The scanner assumes the repo-standard 2-space GitHub Actions | |
| # indentation used in this file and block-scalar `run: |`, | |
| # `run: |-`, and `run: |+` forms. | |
| def step_label(step_name: str | None) -> str: | |
| return step_name or "<unnamed step>" | |
| for lineno, line in enumerate(lines, start=1): | |
| stripped = line.lstrip() | |
| indent = len(line) - len(stripped) | |
| if not stripped or stripped.startswith("#"): | |
| continue | |
| if in_run: | |
| if indent > run_indent: | |
| if expr_open in line: | |
| bad_lines.append((lineno, current_job, step_label(current_step), line.strip())) | |
| continue | |
| in_run = False | |
| run_indent = None | |
| if indent == 0 and stripped == "jobs:": | |
| in_jobs = True | |
| current_job = None | |
| in_steps = False | |
| current_step = None | |
| continue | |
| if indent == 0 and in_jobs: | |
| in_jobs = False | |
| current_job = None | |
| in_steps = False | |
| current_step = None | |
| if not in_jobs: | |
| continue | |
| if indent == 2 and stripped.endswith(":"): | |
| current_job = stripped[:-1] | |
| in_steps = False | |
| current_step = None | |
| continue | |
| if indent == 4 and stripped == "steps:": | |
| in_steps = True | |
| current_step = None | |
| continue | |
| if indent <= 4 and stripped != "steps:": | |
| in_steps = False | |
| current_step = None | |
| if not in_steps: | |
| continue | |
| if indent == 6 and stripped.startswith("- "): | |
| current_step = None | |
| if stripped.startswith("- name:"): | |
| current_step = stripped[len("- name:"):].strip() | |
| continue | |
| if stripped.startswith("- uses:"): | |
| current_step = f"uses {stripped[len('- uses:'):].strip()}" | |
| continue | |
| if stripped.startswith("- run:"): | |
| unsupported.append( | |
| (lineno, current_job, step_label(current_step), "inline list-item run syntax is unsupported") | |
| ) | |
| continue | |
| if indent == 8 and stripped.startswith("name:"): | |
| current_step = stripped[len("name:"):].strip() | |
| continue | |
| if indent == 8 and stripped.startswith("run:"): | |
| marker = stripped[len("run:"):].strip() | |
| if marker not in supported_run_markers: | |
| unsupported.append( | |
| ( | |
| lineno, | |
| current_job, | |
| step_label(current_step), | |
| f"unsupported run syntax: {stripped}", | |
| ) | |
| ) | |
| continue | |
| in_run = True | |
| run_indent = indent | |
| continue | |
| if in_run: | |
| in_run = False | |
| if unsupported or bad_lines: | |
| for lineno, job_name, step_name, message in unsupported: | |
| print( | |
| f"{workflow_path}:{lineno}: job {job_name!r} step {step_name!r}: {message}", | |
| file=sys.stderr, | |
| ) | |
| for lineno, job_name, step_name, content in bad_lines: | |
| print( | |
| f"{workflow_path}:{lineno}: job {job_name!r} step {step_name!r} contains direct workflow interpolation: {content}", | |
| file=sys.stderr, | |
| ) | |
| sys.exit(1) | |
| PY | |
| - name: Ensure install-smoke label exists | |
| run: | | |
| gh label create install-smoke \ | |
| --repo "$REPORT_REPO" \ | |
| --description "Auto-created by the install-smoke workflow" \ | |
| --color "D93F0B" 2>/dev/null || true | |
| - name: Create or update tracking issue | |
| run: | | |
| repo="$REPORT_REPO" | |
| title="Install smoke test failure ($REPORT_TAG)" | |
| run_url="$REPORT_RUN_URL" | |
| body="$(cat <<EOF | |
| ## Install smoke test failure | |
| **Tag:** \`$REPORT_TAG\` | |
| **Run:** $run_url | |
| **Triggered by:** $REPORT_EVENT_NAME | |
| One or more non-gated install channels failed. Check the workflow | |
| run linked above for per-job logs. | |
| _This issue was created automatically by the install-smoke workflow._ | |
| EOF | |
| )" | |
| # Label-based lookup is consistent (no search-index delay). | |
| existing="$(gh issue list --repo "$repo" --state open \ | |
| --label install-smoke \ | |
| --json number -q '.[0].number' 2>/dev/null || true)" | |
| if [[ -n "$existing" ]]; then | |
| echo "Updating existing issue #$existing" | |
| gh issue comment "$existing" --repo "$repo" \ | |
| --body "New failure for tag \`$REPORT_TAG\`: $run_url" | |
| else | |
| echo "Creating new tracking issue" | |
| gh issue create --repo "$repo" \ | |
| --title "$title" \ | |
| --body "$body" \ | |
| --label install-smoke | |
| fi |