Indexing dashboard: generation column and spawn-aware prerender job links #87
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: Observability — PR preview | |
| # Spins up a per-PR preview of the dashboards a PR changes inside the staging | |
| # Grafana, under per-PR-prefixed UIDs (`pr<n>-*`) in a per-PR folder so the | |
| # preview coexists with the canonical staging copies without overwriting | |
| # them. On PR close the preview is torn down. A separate sweep workflow | |
| # (observability-preview-sweep.yml) is the safety net for closes the | |
| # preview-cleanup job missed. | |
| # | |
| # Scope: dashboards + folders only. Data sources, alert rules, and the | |
| # home-dashboard preference are file-provisioned at Grafana startup and | |
| # can't be previewed per-PR — the existing observability-diff.yml comment | |
| # already surfaces their changes. CS-11106. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, closed] | |
| paths: | |
| - "packages/observability/**" | |
| - ".github/workflows/observability-preview.yml" | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pull-requests: write | |
| concurrency: | |
| # Serialize per-PR so a force-push after an open doesn't race the previous | |
| # push, but allow different PRs to run in parallel. Don't cancel in-flight — | |
| # we never want to leave the preview in a half-pushed state. | |
| group: observability-preview-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| env: | |
| AWS_REGION: us-east-1 | |
| # Reuses the apply role from cardstack/infra#665 (same role | |
| # observability-apply-staging.yml uses). Permissions cover every SSM path | |
| # below — see that workflow's header for the up-to-date list. | |
| AWS_ROLE_ARN: arn:aws:iam::680542703984:role/boxel-observability-apply | |
| GRAFANACTL_VERSION: "0.1.10" | |
| PREVIEW_MARKER: "<!-- observability-preview -->" | |
| STAGING_GRAFANA_BASE_URL: "https://dashboard-staging.stack.cards" | |
| jobs: | |
| preview-apply: | |
| name: Apply preview to staging | |
| # Fork-PR guard: this job assumes AWS and pushes to staging Grafana, so | |
| # it must not run on PRs whose code we don't trust. Same pattern as | |
| # observability-diff.yml and observability-apply-staging.yml. | |
| if: >- | |
| github.event.action != 'closed' | |
| && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| # render-preview.sh runs `git diff origin/<base>...HEAD` to detect | |
| # changed dashboards, so we need full history to reach the merge-base | |
| # (same constraint as observability-diff.yml). | |
| fetch-depth: 0 | |
| - name: Fetch PR base ref | |
| # Ensure origin/<base> is fully available — no `--depth=1`. | |
| # A shallow base makes `git diff origin/<base>...HEAD` fail with | |
| # "no merge base" whenever main has advanced past the PR's true | |
| # merge base (i.e., any PR in flight long enough for a merge to | |
| # land on main). Unbounded fetch is cheap against the full repo | |
| # the checkout step already pulled (fetch-depth: 0). | |
| env: | |
| PR_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: git fetch --no-tags origin "${PR_BASE_REF}" | |
| - name: Install grafanactl | |
| uses: ./.github/actions/install-grafanactl | |
| with: | |
| version: ${{ env.GRAFANACTL_VERSION }} | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 | |
| with: | |
| role-to-assume: ${{ env.AWS_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Source preview-time secrets from SSM | |
| # Subset of what observability-apply-staging.yml fetches. The preview | |
| # only touches dashboards + folders, so we don't need the LOKI_URL / | |
| # BOXEL_DB_* / WORKER_LOG_* set — those are read by apply-datasources | |
| # / apply-alerting, which apply-preview.sh never invokes. | |
| run: | | |
| GRAFANA_TOKEN="$(aws ssm get-parameter \ | |
| --name /staging/grafana/grafanactl_token \ | |
| --with-decryption \ | |
| --query 'Parameter.Value' \ | |
| --output text)" | |
| echo "::add-mask::$GRAFANA_TOKEN" | |
| echo "GRAFANA_TOKEN=$GRAFANA_TOKEN" >> "$GITHUB_ENV" | |
| REALM_SERVER_URL="$(aws ssm get-parameter \ | |
| --name /staging/boxel-grafana/realm_server_url \ | |
| --query 'Parameter.Value' \ | |
| --output text)" | |
| echo "REALM_SERVER_URL=$REALM_SERVER_URL" >> "$GITHUB_ENV" | |
| GRAFANA_SECRET="$(aws ssm get-parameter \ | |
| --name /staging/boxel/GRAFANA_SECRET \ | |
| --with-decryption \ | |
| --query 'Parameter.Value' \ | |
| --output text)" | |
| echo "::add-mask::$GRAFANA_SECRET" | |
| echo "GRAFANA_SECRET=$GRAFANA_SECRET" >> "$GITHUB_ENV" | |
| - name: Apply preview | |
| id: apply | |
| working-directory: packages/observability | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| run: | | |
| set -eo pipefail | |
| OUT_FILE="$(mktemp)" | |
| ./scripts/apply-preview.sh \ | |
| --pr "$PR_NUMBER" \ | |
| --base-ref "origin/${PR_BASE_REF}" \ | |
| --env staging \ | |
| > "$OUT_FILE" | |
| echo "out_file=$OUT_FILE" >> "$GITHUB_OUTPUT" | |
| if [[ -s "$OUT_FILE" ]]; then | |
| echo "empty=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "empty=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| echo "::group::Preview summary" | |
| cat "$OUT_FILE" | |
| echo "::endgroup::" | |
| - name: Cleanup stale preview (no changes left) | |
| # When the current head of the PR has no dashboard changes vs base, | |
| # apply-preview emits empty output and pushes nothing. But a prior | |
| # push to the same PR may have already applied a preview that's | |
| # still live in staging — revert/rebase scenarios. Run cleanup so | |
| # the PR's staging footprint matches its current head; otherwise | |
| # the daily sweep wouldn't catch this until PR close. | |
| if: steps.apply.outputs.empty == 'true' | |
| working-directory: packages/observability | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: ./scripts/cleanup-preview.sh --pr "$PR_NUMBER" --env staging | |
| - name: Post sticky PR comment | |
| # Pinned to SHA (matches observability-diff.yml's reasoning): | |
| # pull-requests: write + AWS role = non-trivial supply-chain exposure. | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| env: | |
| OUT_FILE: ${{ steps.apply.outputs.out_file }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = process.env.PREVIEW_MARKER; | |
| const grafanaBase = process.env.STAGING_GRAFANA_BASE_URL; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const raw = fs.readFileSync(process.env.OUT_FILE, 'utf8').trim(); | |
| // Find any prior preview sticky on this PR; we either update or | |
| // delete it based on whether there's anything to preview now. | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }, | |
| ); | |
| const existing = comments.find( | |
| c => c.body && c.body.startsWith(marker), | |
| ); | |
| if (!raw) { | |
| // No dashboards changed at this HEAD. Don't post a "nothing | |
| // to see" comment — if a prior push left a sticky behind, | |
| // delete it so the PR's comment stream matches its current | |
| // state. (Stale preview *resources* from a prior push are | |
| // handled by the "Cleanup stale preview" step above.) | |
| if (existing) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| }); | |
| core.info(`Deleted stale sticky comment ${existing.id}`); | |
| } else { | |
| core.info('No dashboard changes — nothing to post.'); | |
| } | |
| return; | |
| } | |
| const folders = []; | |
| const dashboards = []; | |
| for (const line of raw.split('\n')) { | |
| // Format: `<KIND>:<uid>:<title>` (title may contain `:`). | |
| const m = line.match(/^(FOLDER|DASHBOARD):([^:]+):(.*)$/); | |
| if (!m) continue; | |
| const [, kind, uid, title] = m; | |
| (kind === 'FOLDER' ? folders : dashboards).push({ uid, title }); | |
| } | |
| const folderLines = folders.map(f => | |
| `- **Folder:** [${f.title}](${grafanaBase}/dashboards/f/${f.uid}/)`, | |
| ); | |
| const dashboardLines = dashboards.map(d => | |
| `- [${d.title}](${grafanaBase}/d/${d.uid}/)`, | |
| ); | |
| const body = [ | |
| marker, | |
| '## Grafana preview', | |
| '', | |
| `Preview deployed for **${dashboards.length} dashboard${dashboards.length === 1 ? '' : 's'}** in the staging Grafana.`, | |
| 'Cross-dashboard drill-throughs still point at the canonical staging dashboards.', | |
| '', | |
| ...folderLines, | |
| '', | |
| '**Dashboards:**', | |
| ...dashboardLines, | |
| '', | |
| 'Preview is torn down automatically when this PR is closed or merged.', | |
| '', | |
| `_(Run: ${runUrl})_`, | |
| ].join('\n'); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| core.info(`Updated sticky comment ${existing.id}`); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| core.info('Created sticky comment'); | |
| } | |
| preview-cleanup: | |
| name: Cleanup preview from staging | |
| # Cleanup runs on the `closed` action (covers both merge and close- | |
| # without-merge). Same fork-PR guard — cleanup hits staging. | |
| if: >- | |
| github.event.action == 'closed' | |
| && github.event.pull_request.head.repo.full_name == github.repository | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Install grafanactl | |
| uses: ./.github/actions/install-grafanactl | |
| with: | |
| version: ${{ env.GRAFANACTL_VERSION }} | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 | |
| with: | |
| role-to-assume: ${{ env.AWS_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Source GRAFANA_TOKEN | |
| # Cleanup doesn't need any of the URL/secret substitution values — | |
| # it lists live state and deletes by selector. Only the grafanactl | |
| # token matters. | |
| run: | | |
| GRAFANA_TOKEN="$(aws ssm get-parameter \ | |
| --name /staging/grafana/grafanactl_token \ | |
| --with-decryption \ | |
| --query 'Parameter.Value' \ | |
| --output text)" | |
| echo "::add-mask::$GRAFANA_TOKEN" | |
| echo "GRAFANA_TOKEN=$GRAFANA_TOKEN" >> "$GITHUB_ENV" | |
| - name: Cleanup preview | |
| working-directory: packages/observability | |
| env: | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: ./scripts/cleanup-preview.sh --pr "$PR_NUMBER" --env staging | |
| - name: Delete sticky PR comment | |
| # After cleanup, there's nothing left to point at — delete the | |
| # sticky entirely rather than leaving a "preview cleaned up" | |
| # gravestone. Matches the apply-time policy of not posting a | |
| # comment when there's nothing to show. | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const marker = process.env.PREVIEW_MARKER; | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| per_page: 100, | |
| }, | |
| ); | |
| const existing = comments.find( | |
| c => c.body && c.body.startsWith(marker), | |
| ); | |
| if (existing) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| }); | |
| core.info(`Deleted sticky comment ${existing.id}`); | |
| } else { | |
| core.info('No preview comment to delete — skipping.'); | |
| } |