feat: emit OpenTelemetry exemplars from HTTP duration histogram (#826) #60
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: Release | ||
|
Check failure on line 1 in .github/workflows/release.yml
|
||
| # Trigger when a semantic version tag is pushed (e.g. v1.2.3, v0.4.0-rc.1). | ||
| # Maintained releases only; pre-releases and patch lines produce the same pipeline. | ||
| on: | ||
| push: | ||
| tags: | ||
| - "v[0-9]+.[0-9]+.[0-9]+*" | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: "Tag to release (without the leading 'v'). Used for manual repro." | ||
| required: true | ||
| default: "" | ||
| # Hard-deny everything by default; each job opts in to what it actually needs. | ||
| permissions: {} | ||
| concurrency: | ||
| # Only one release pipeline runs at a time per ref to avoid duplicate pushes. | ||
| group: release-${{ github.ref }} | ||
| cancel-in-progress: false | ||
| jobs: | ||
| # --------------------------------------------------------------------------- | ||
| # 1. Build & publish a multi-arch image to GHCR. | ||
| # --------------------------------------------------------------------------- | ||
| build: | ||
| name: Build & push image | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read # checkout | ||
| packages: write # push image + signature layers to GHCR | ||
| id-token: write # exchange for Fulcio certificate during cosign sign | ||
| outputs: | ||
| image: ${{ env.IMAGE }} | ||
| digest: ${{ steps.build.outputs.digest }} | ||
| tag: ${{ steps.meta.outputs.tag }} | ||
| name: release-sdk | ||
| # Publish the @stellabill/sdk TypeScript SDK to npm whenever a GitHub | ||
| # release is published. The release workflow: | ||
| # 1. Generates TypeScript types from openapi/openapi.yaml | ||
| # 2. Builds ESM + CJS + .d.ts bundles via tsup | ||
| # 3. Runs the full vitest suite with the >=95% coverage gate | ||
| # 4. Bumps the SDK package version to the release tag | ||
| # 5. Publishes to npm with provenance under the trusted-publisher OIDC | ||
| # | ||
| # Manual dispatch is supported with a `tag` input for back-filling. | ||
| on: | ||
| release: | ||
| types: [published] | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: "Release tag (e.g. v0.2.0). Defaults to the latest release tag." | ||
| required: false | ||
| default: '' | ||
| type: string | ||
| permissions: | ||
| id-token: write # Required for npm OIDC trusted publishing (provenance) | ||
| contents: read | ||
| jobs: | ||
| test-sdk: | ||
| name: Test (generate, build, vitest >=95% coverage) | ||
| runs-on: ubuntu-latest | ||
| defaults: | ||
| run: | ||
| working-directory: sdks/ts | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Resolve image name | ||
| id: meta | ||
| env: | ||
| REPO: ${{ github.repository }} | ||
| run: | | ||
| # GHCR is lowercase-only. Convert via bash parameter expansion rather | ||
| # than a non-existent GitHub Actions expression. | ||
| IMAGE="ghcr.io/${REPO,,}" | ||
| echo "image=${IMAGE}" >> "$GITHUB_OUTPUT" | ||
| TAG="${GITHUB_REF_NAME}" | ||
| if [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then | ||
| : # tag already in the proper form | ||
| else | ||
| # workflow_dispatch fallback: use SHA | ||
| TAG="sha-${GITHUB_SHA::7}" | ||
| fi | ||
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | ||
| - name: Set up QEMU | ||
| uses: docker/setup-qemu-action@v3 | ||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Build & push (multi-arch) | ||
| id: build | ||
| uses: docker/build-push-action@v5 | ||
| with: | ||
| context: . | ||
| file: ./Dockerfile | ||
| platforms: linux/amd64,linux/arm64 | ||
| push: true | ||
| tags: | | ||
| ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.tag }} | ||
| ${{ steps.meta.outputs.image }}:latest | ||
| labels: | | ||
| org.opencontainers.image.title=stellabill-backend | ||
| org.opencontainers.image.source=https://github.com/${{ github.repository }} | ||
| org.opencontainers.image.revision=${{ github.sha }} | ||
| org.opencontainers.image.version=${{ steps.meta.outputs.tag }} | ||
| build-args: | | ||
| VERSION=${{ steps.meta.outputs.tag }} | ||
| COMMIT=${{ github.sha }} | ||
| BUILD_TIME=${{ github.event.repository.updated_at }} | ||
| provenance: true # produce SLSA-style provenance in image manifest | ||
| sbom: true # produce SBOM layer (syft) | ||
| - name: Export IMAGE for downstream jobs | ||
| run: | | ||
| echo "IMAGE=${{ steps.meta.outputs.image }}" >> "$GITHUB_ENV" | ||
| echo "TAG=${{ steps.meta.outputs.tag }}" >> "$GITHUB_ENV" | ||
| echo "DIGEST=${{ steps.build.outputs.digest }}" >> "$GITHUB_ENV" | ||
| # --------------------------------------------------------------------------- | ||
| # 2. Generate a SLSA v1 provenance predicate (minimal hand-rolled in-toto | ||
| # statement). This is intentionally simpler than the slsa-github-generator | ||
| # framework so it stays auditable end-to-end. | ||
| # --------------------------------------------------------------------------- | ||
| provenance: | ||
| name: Generate SLSA provenance | ||
| needs: build | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| outputs: | ||
| predicate-path: ${{ steps.write.outputs.path }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| - name: Resolve image digest | ||
| id: resolve | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| DIGEST: ${{ env.DIGEST }} | ||
| run: | | ||
| if [ -z "${IMAGE}" ] || [ -z "${DIGEST}" ]; then | ||
| echo "IMAGE or DIGEST missing - previous job did not export" >&2 | ||
| exit 1 | ||
| fi | ||
| # strip the sha256: prefix for in-toto digest field | ||
| SHORT_DIGEST="${DIGEST#sha256:}" | ||
| echo "subject=${IMAGE}@${DIGEST}" >> "$GITHUB_OUTPUT" | ||
| echo "short=${SHORT_DIGEST}" >> "$GITHUB_OUTPUT" | ||
| - name: Build SLSA v1 predicate | ||
| id: write | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| SHORT_DIGEST: ${{ steps.resolve.outputs.short }} | ||
| SHA: ${{ github.sha }} | ||
| RUN_ID: ${{ github.run_id }} | ||
| RUN_ATTEMPT: ${{ github.run_attempt }} | ||
| WORKFLOW_REF: ${{ github.workflow_ref }} | ||
| REF: ${{ github.ref }} | ||
| run: | | ||
| mkdir -p artifacts | ||
| cat > artifacts/provenance.intoto.jsonl <<EOF | ||
| {"_type":"https://in-toto.io/Statement/v1","predicateType":"https://slsa.dev/provenance/v1","subject":[{"name":"${IMAGE}","digest":{"sha256":"${SHORT_DIGEST}"}}],"predicate":{"buildDefinition":{"buildType":"https://github.com/actions/workflow/v1","externalParameters":{"workflow":".github/workflows/release.yml","ref":"${REF}"},"resolvedDependencies":[{"name":"source","uri":"git+https://github.com/${GITHUB_REPOSITORY}@${SHA}"}]},"runDetails":{"builder":{"id":"https://github.com/actions/runner"},"metadata":{"invocationId":"https://github.com/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/attempts/${RUN_ATTEMPT}"}}}} | ||
| EOF | ||
| echo "path=$(pwd)/artifacts/provenance.intoto.jsonl" >> "$GITHUB_OUTPUT" | ||
| # --------------------------------------------------------------------------- | ||
| # 3. Sign the published image digest with cosign keyless using the GitHub | ||
| # OIDC token (Fulcio issues a short-lived certificate bound to the workflow). | ||
| # --------------------------------------------------------------------------- | ||
| sign: | ||
| name: Cosign sign (keyless) | ||
| needs: [build, provenance] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| id-token: write # required for keyless signing | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3.5.0 | ||
| with: | ||
| cosign-release: 'v2.4.1' | ||
| - name: Sign image by digest | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| DIGEST: ${{ env.DIGEST }} | ||
| run: | | ||
| # Pin the signing target to the digest, not the tag, so a future | ||
| # re-tag of the tag cannot invalidate the signature. | ||
| # --oidc-provider=github explicitly declares the GH Actions OIDC | ||
| # issuer to Fulcio; cosign would auto-detect it anyway, but the | ||
| # explicit flag is required by the project issue (#437) and makes | ||
| # the trust path auditable. | ||
| cosign sign --yes \ | ||
| --oidc-provider=github \ | ||
| "${IMAGE}@${DIGEST}" | ||
| # --- Attach SLSA provenance as a cosign attestation layer --- | ||
| - name: Attest SLSA provenance | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| DIGEST: ${{ env.DIGEST }} | ||
| PREDICATE: ${{ needs.provenance.outputs.predicate-path }} | ||
| run: | | ||
| # The provenance job produces a FULL in-toto statement (with | ||
| # _type/predicateType/subject/predicate), so we use --statement. | ||
| # Using --predicate here would cause cosign to wrap an already | ||
| # wrapped statement and the resulting attestation would not match | ||
| # what Kyverno expects. | ||
| # --oidc-provider=github mirrors the signing identity (Fulcio bound | ||
| # to GH Actions OIDC) so the attestation lands on Rekor under the | ||
| # same certificate identity as the signature. | ||
| cosign attest --yes \ | ||
| --type slsaprovenance \ | ||
| --oidc-provider=github \ | ||
| --statement "${PREDICATE}" \ | ||
| "${IMAGE}@${DIGEST}" | ||
| # --------------------------------------------------------------------------- | ||
| # 4. Re-verify the signature in the same job graph before declaring success. | ||
| # This is the same command a cluster operator would run on a workstation. | ||
| # --------------------------------------------------------------------------- | ||
| verify: | ||
| name: Cosign verify (keyless) | ||
| needs: sign | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write # required by cosign verify-blob / verify for client state | ||
| steps: | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3.5.0 | ||
| with: | ||
| cosign-release: 'v2.4.1' | ||
| - name: Verify image signature | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| DIGEST: ${{ env.DIGEST }} | ||
| run: | | ||
| # Pin the organisation to known-good forks/upstream. `*` would let an | ||
| # attacker publish a repo called `stellabill-backend` and impersonate. | ||
| cosign verify \ | ||
| --certificate-identity-regexp 'https://github.com/(Stellabill|believetimothy)/stellabill-backend/.github/workflows/release.yml@refs/tags/.+' \ | ||
| --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ | ||
| --output text \ | ||
| "${IMAGE}@${DIGEST}" | ||
| - name: Verify SLSA provenance attestation | ||
| env: | ||
| IMAGE: ${{ env.IMAGE }} | ||
| DIGEST: ${{ env.DIGEST }} | ||
| run: | | ||
| cosign verify-attestation \ | ||
| --type slsaprovenance \ | ||
| --certificate-identity-regexp 'https://github.com/(Stellabill|believetimothy)/stellabill-backend/.github/workflows/release.yml@refs/tags/.+' \ | ||
| --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ | ||
| --output text \ | ||
| "${IMAGE}@${DIGEST}" | ||
| - name: Checkout release tag | ||
| if: github.event_name == 'release' | ||
| run: git checkout ${{ github.event.release.tag_name }} | ||
| - name: Setup Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 10 | ||
| - name: Install dependencies | ||
| run: pnpm install | ||
| - name: Generate types from openapi.yaml | ||
| run: pnpm run generate | ||
| - name: Build (tsup) | ||
| run: pnpm run build | ||
| - name: Typecheck | ||
| run: pnpm run typecheck | ||
| - name: Test + coverage gate (>=95%) | ||
| run: | | ||
| set -euo pipefail | ||
| pnpm run test | ||
| - name: Upload SDK dist artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: sdk-dist | ||
| path: | | ||
| sdks/ts/dist | ||
| sdks/ts/package.json | ||
| retention-days: 14 | ||
| - name: Upload coverage artifact | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: sdk-coverage | ||
| path: sdks/ts/coverage | ||
| retention-days: 14 | ||
| publish-sdk: | ||
| name: Publish to npm | ||
| needs: test-sdk | ||
| runs-on: ubuntu-latest | ||
| # Publish only on real GitHub releases (or explicit dispatch with a tag) | ||
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | ||
| defaults: | ||
| run: | ||
| working-directory: sdks/ts | ||
| steps: | ||
| - name: Download SDK dist artifact | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: sdk-dist | ||
| path: sdks/ts/dist | ||
| - name: Checkout source matching the release tag | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| ref: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }} | ||
| - name: Setup Node | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| registry-url: 'https://registry.npmjs.org' | ||
| # Trust the OIDC publisher declared in sdks/ts/package.json | ||
| - name: Set SDK version to release tag | ||
| run: | | ||
| set -euo pipefail | ||
| if [ "${{ github.event_name }}" = "release" ]; then | ||
| TAG="${{ github.event.release.tag_name }}" | ||
| else | ||
| TAG="${{ inputs.tag }}" | ||
| fi | ||
| if [ -z "${TAG:-}" ] || [ "${TAG}" = " " ]; then | ||
| echo "::error::No release tag available. Pass a 'tag' input when using workflow_dispatch." | ||
| exit 1 | ||
| fi | ||
| # Strip leading 'v' from the tag (e.g. v0.2.0 -> 0.2.0) | ||
| VERSION="${TAG#v}" | ||
| echo "Publishing @stellabill/sdk@${VERSION}" | ||
| npm pkg set "version=${VERSION}" | ||
| - name: Validate package.json | ||
| run: | | ||
| set -euo pipefail | ||
| node -e "const p=require('./package.json'); \ | ||
| if(!p.name || !p.version){throw new Error('missing name/version')}; \ | ||
| if(!p.exports?.['.']?.types){throw new Error('missing types export')}; \ | ||
| if(!p.dependencies?.['openapi-fetch']){throw new Error('missing openapi-fetch dep')}; \ | ||
| console.log('package.json OK')" | ||
| - name: Sanity-check that dist is built and types exist | ||
| run: | | ||
| set -euo pipefail | ||
| test -f dist/index.js | ||
| test -f dist/index.cjs | ||
| test -f dist/index.d.ts | ||
| echo "dist OK" | ||
| - name: Publish (trusted publishing via OIDC) | ||
| run: npm publish --provenance --access public | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| # OIDC trusted publishing replaces NODE_AUTH_TOKEN when configured at | ||
| # https://docs.npmjs.com/generating-provenance-statements; we still | ||
| # fall back to NPM_TOKEN to remain compatible with non-OIDC setups. | ||