Build Windows MSI artifacts from short dist path #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
| # Reusable Release Workflow for ConductorOne Connectors | ||
| # | ||
| # Documentation: | ||
| # - docs/release-workflow.md - Pipeline overview, security properties, testing | ||
| # - docs/diagrams/DIAGRAM_RULES.md - When to update documentation | ||
| # | ||
| # When modifying this file, update documentation if you change: | ||
| # - Job structure or dependencies → update docs/diagrams/release-workflow.dot | ||
| # - Security properties → update docs/release-workflow.md | ||
| # - Run `make docs` to regenerate the diagram | ||
| name: Reusable Release Workflow | ||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| tag: | ||
| required: true | ||
| type: string | ||
| lambda: | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| description: "Whether to release with Lambda image support." | ||
| docker: | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| description: "Whether to release with Docker image support." | ||
| dockerfile_template: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| description: "Path to a custom Dockerfile template in the caller repo (relative to repo root). Only valid when lambda is false. Supports ${REPO_NAME} substitution." | ||
| docker_extra_files: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| description: "Comma-separated list of extra files/directories from the caller repo to include in the Docker build context (e.g., 'java,config'). Only valid when dockerfile_template is set." | ||
| msi: | ||
| required: false | ||
| type: boolean | ||
| default: true | ||
| description: "Whether to build MSI Windows installers." | ||
| msi_wxs_path: | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| description: "Path to a custom WXS file in the caller repo for MSI generation (relative to repo root). If not provided, uses default template." | ||
| secrets: | ||
| RELENG_GITHUB_TOKEN: | ||
| required: true | ||
| APPLE_SIGNING_KEY_P12: | ||
| required: true | ||
| APPLE_SIGNING_KEY_P12_PASSWORD: | ||
| required: true | ||
| AC_PASSWORD: | ||
| required: true | ||
| AC_PROVIDER: | ||
| required: true | ||
| DATADOG_API_KEY: | ||
| required: true | ||
| GORELEASER_PRO_KEY: | ||
| required: false | ||
| description: "GoReleaser Pro license key for MSI builds. Required when msi is true." | ||
| env: | ||
| CDN_BASE_URL: "https://dist.conductorone.com" | ||
| S3_BUCKET: "connector-artifact-registry" | ||
| GENERATED_DIR: "_generated" | ||
| permissions: {} | ||
| jobs: | ||
| validate-inputs: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Validate tag format | ||
| env: | ||
| TAG: ${{ inputs.tag }} | ||
| run: | | ||
| # Strict semver regex with 'v' prefix (per https://semver.org) | ||
| # Supports: v1.2.3, v1.2.3-alpha, v1.2.3-alpha.1, v1.2.3+build, v1.2.3-rc.1+build.123 | ||
| SEMVER_REGEX='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$' | ||
| if [[ ! "$TAG" =~ $SEMVER_REGEX ]]; then | ||
| echo "::error::Tag must be valid semver starting with 'v' (e.g., v1.2.3, v1.0.0-rc.1). Got: $TAG" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Tag format valid: $TAG" | ||
| - name: Validate dockerfile_template requires lambda=false | ||
| if: inputs.dockerfile_template != '' && inputs.lambda == true | ||
| run: | | ||
| echo "::error::dockerfile_template can only be used when lambda is false" | ||
| exit 1 | ||
| - name: Validate dockerfile_template has safe path | ||
| if: inputs.dockerfile_template != '' | ||
| env: | ||
| DOCKERFILE_TEMPLATE: ${{ inputs.dockerfile_template }} | ||
| run: | | ||
| if [[ "$DOCKERFILE_TEMPLATE" == /* ]] || [[ "$DOCKERFILE_TEMPLATE" == *".."* ]] || [[ ! "$DOCKERFILE_TEMPLATE" =~ ^[A-Za-z0-9._/-]+$ ]]; then | ||
| echo "::error::dockerfile_template must be a relative path with safe characters and without '..' traversal. Got: $DOCKERFILE_TEMPLATE" | ||
| exit 1 | ||
| fi | ||
| - name: Validate docker_extra_files requires dockerfile_template | ||
| if: inputs.docker_extra_files != '' && inputs.dockerfile_template == '' | ||
| run: | | ||
| echo "::error::docker_extra_files can only be used when dockerfile_template is set" | ||
| exit 1 | ||
| - name: Validate docker_extra_files have safe paths | ||
| if: inputs.docker_extra_files != '' | ||
| env: | ||
| DOCKER_EXTRA_FILES: ${{ inputs.docker_extra_files }} | ||
| run: | | ||
| IFS=',' read -ra FILES <<< "$DOCKER_EXTRA_FILES" | ||
| for file in "${FILES[@]}"; do | ||
| file="${file#"${file%%[![:space:]]*}"}" | ||
| file="${file%"${file##*[![:space:]]}"}" | ||
| if [[ -z "$file" ]] || [[ "$file" == /* ]] || [[ "$file" == *".."* ]] || [[ ! "$file" =~ ^[A-Za-z0-9._/-]+$ ]]; then | ||
| echo "::error::docker_extra_files entries must be relative paths with safe characters and without '..' traversal. Got: $file" | ||
| exit 1 | ||
| fi | ||
| done | ||
| - name: Validate msi_wxs_path has no path traversal | ||
| if: inputs.msi_wxs_path != '' | ||
| env: | ||
| WXS_PATH: ${{ inputs.msi_wxs_path }} | ||
| run: | | ||
| if [[ "$WXS_PATH" == /* ]] || [[ "$WXS_PATH" == *".."* ]] || [[ ! "$WXS_PATH" =~ ^[A-Za-z0-9._/-]+$ ]]; then | ||
| echo "::error::msi_wxs_path must be a relative path with safe characters and without '..' traversal. Got: $WXS_PATH" | ||
| exit 1 | ||
| fi | ||
| - name: Validate GORELEASER_PRO_KEY when msi enabled | ||
| if: inputs.msi == true | ||
| env: | ||
| HAS_KEY: ${{ secrets.GORELEASER_PRO_KEY != '' }} | ||
| run: | | ||
| if [ "$HAS_KEY" != "true" ]; then | ||
| echo "::error::GORELEASER_PRO_KEY secret is required when msi is true" | ||
| exit 1 | ||
| fi | ||
| determine-workflows-ref: | ||
| needs: validate-inputs | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| actions: read | ||
| outputs: | ||
| ref: ${{ steps.workflow-version.outputs.sha }} | ||
| steps: | ||
| - name: Determine workflows ref | ||
| id: workflow-version | ||
| uses: canonical/get-workflow-version-action@v1 | ||
| with: | ||
| repository-name: "ConductorOne/github-workflows" | ||
| file-name: "release.yaml" | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| goreleaser-binaries: | ||
| needs: determine-workflows-ref | ||
| runs-on: macos-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write # <-- needed for cosign keyless (OIDC) | ||
| outputs: | ||
| s3_directory: ${{ steps.s3-directory.outputs.S3_DIRECTORY }} | ||
| binaries_manifest: ${{ steps.generate-binaries-manifest.outputs.binaries_manifest }} | ||
| binaries_checksums: ${{ steps.output-checksums.outputs.checksums }} | ||
| steps: | ||
| - name: Checkout caller repo | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _caller | ||
| repository: ${{ github.event.repository.full_name }} | ||
| ref: refs/tags/${{ inputs.tag }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: Verify caller checkout matches release tag | ||
| working-directory: _caller | ||
| shell: bash | ||
| env: | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")" | ||
| head_commit="$(git rev-parse HEAD)" | ||
| if [ "$head_commit" != "$tag_commit" ]; then | ||
| echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit" | ||
| exit 1 | ||
| fi | ||
| echo "Verified $RELEASE_TAG at $head_commit" | ||
| - name: Checkout connector workflows | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _workflows | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| persist-credentials: false | ||
| - name: Derive AWS role names | ||
| id: role-names | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| REPO_OWNER: ${{ github.event.repository.owner.login }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| run: | | ||
| bash ./scripts/derive-iam-role-name.sh \ | ||
| --prefix GHA-Artifacts- \ | ||
| --suffix "${REPO_OWNER}-${REPO_NAME}" \ | ||
| --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" | ||
| - name: Set up Go for caller | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_caller/go.mod" | ||
| cache: false | ||
| - name: Calculate S3 directory | ||
| id: s3-directory | ||
| shell: bash | ||
| run: | | ||
| ORG="${{ github.event.repository.owner.login }}" | ||
| REPO="${{ github.event.repository.name }}" | ||
| TAG="${{ inputs.tag }}" | ||
| echo "S3_DIRECTORY=releases/$ORG/$REPO/$TAG" >> "$GITHUB_OUTPUT" | ||
| - name: Generate configs for binaries | ||
| working-directory: _workflows | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| S3_BUCKET: ${{ env.S3_BUCKET }} | ||
| S3_REGION: "us-west-2" | ||
| S3_DIRECTORY: ${{ steps.s3-directory.outputs.S3_DIRECTORY }} | ||
| # For provenance predicate template | ||
| WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| mkdir -p "${GENERATED_DIR}" | ||
| export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | ||
| envsubst < .gon-amd64-template.json | tee "${GENERATED_DIR}/.gon-amd64.json" | ||
| envsubst < .gon-arm64-template.json | tee "${GENERATED_DIR}/.gon-arm64.json" | ||
| envsubst < templates/.goreleaser-binaries-template.yaml.tmpl | tee "${GENERATED_DIR}/.goreleaser.binaries.yaml" | ||
| envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "${GENERATED_DIR}/predicate.json" | ||
| - name: Set up Gon | ||
| run: brew tap conductorone/gon && brew install conductorone/gon/gon | ||
| - name: Import Keychain Certs | ||
| uses: apple-actions/import-codesign-certs@v1 | ||
| with: | ||
| p12-file-base64: ${{ secrets.APPLE_SIGNING_KEY_P12 }} | ||
| p12-password: ${{ secrets.APPLE_SIGNING_KEY_P12_PASSWORD }} | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3 | ||
| - name: Download syft | ||
| uses: anchore/sbom-action/download-syft@v0 | ||
| - name: Configure AWS credentials via OIDC | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| role-to-assume: arn:aws:iam::025044153841:role/${{ steps.role-names.outputs.gha_artifacts_role_name }} | ||
| aws-region: us-west-2 | ||
| - name: Run GoReleaser | ||
| uses: goreleaser/goreleaser-action@v6 | ||
| with: | ||
| workdir: _caller | ||
| version: "~> v2.13" | ||
| args: release --clean --config ../_workflows/_generated/.goreleaser.binaries.yaml | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} | ||
| AC_PASSWORD: ${{ secrets.AC_PASSWORD }} | ||
| AC_PROVIDER: ${{ secrets.AC_PROVIDER }} | ||
| - name: Generate SLSA provenance for archives | ||
| working-directory: _workflows | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| PROVENANCE_COUNT=0 | ||
| # Find all downloadable archives (not checksums, not signatures, not sboms) | ||
| for artifact in "${CALLER_DIST}"/*.zip "${CALLER_DIST}"/*.tar.gz; do | ||
| [ -f "$artifact" ] || continue | ||
| # Skip if it's not an archive we want to attest | ||
| [[ "$artifact" == *checksums* ]] && continue | ||
| BASENAME=$(basename "$artifact") | ||
| echo "Generating provenance for: $BASENAME" | ||
| cosign attest-blob \ | ||
| --yes \ | ||
| --predicate "${GENERATED_DIR}/predicate.json" \ | ||
| --type slsaprovenance1 \ | ||
| --bundle "${CALLER_DIST}/${BASENAME}.provenance.sigstore.json" \ | ||
| "$artifact" > /dev/null | ||
| echo "✅ Created ${BASENAME}.provenance.sigstore.json" | ||
| ((PROVENANCE_COUNT++)) || true | ||
| done | ||
| # Note: checksums provenance is generated in publish-release-manifest job | ||
| # after merging with Windows hashes | ||
| echo "Generated provenance bundles: ${PROVENANCE_COUNT}" | ||
| if [ "$PROVENANCE_COUNT" -eq 0 ]; then | ||
| echo "::error::No provenance bundles were generated - this indicates a build problem" | ||
| exit 1 | ||
| fi | ||
| ls "${CALLER_DIST}"/*.provenance.sigstore.json | ||
| - name: Sign SBOMs as attestation bundles | ||
| working-directory: _workflows | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| SIGNED_COUNT=0 | ||
| # Find all SBOM files generated by GoReleaser (syft) | ||
| # GoReleaser names SBOMs as: archive.zip.sbom.json or archive.tar.gz.sbom.json | ||
| for sbom in "${CALLER_DIST}"/*.sbom.json; do | ||
| [ -f "$sbom" ] || continue | ||
| # Get the archive filename (remove .sbom.json suffix) | ||
| # e.g., "baton-foo-v1.0.0-darwin-amd64.zip.sbom.json" -> "baton-foo-v1.0.0-darwin-amd64.zip" | ||
| SBOM_BASENAME=$(basename "$sbom") | ||
| ARCHIVE_NAME="${SBOM_BASENAME%.sbom.json}" | ||
| # The archive name after stripping .sbom.json already includes the extension (.zip or .tar.gz) | ||
| ARCHIVE="${CALLER_DIST}/${ARCHIVE_NAME}" | ||
| if [ ! -f "$ARCHIVE" ]; then | ||
| echo "::error::Could not find archive for SBOM: $sbom (expected: $ARCHIVE)" | ||
| exit 1 | ||
| fi | ||
| echo "Signing SBOM for: $(basename "$ARCHIVE")" | ||
| cosign attest-blob \ | ||
| --yes \ | ||
| --predicate "$sbom" \ | ||
| --type https://spdx.dev/Document \ | ||
| --bundle "${ARCHIVE}.sbom.sigstore.json" \ | ||
| "$ARCHIVE" > /dev/null | ||
| echo "✅ Created $(basename "$ARCHIVE").sbom.sigstore.json" | ||
| ((SIGNED_COUNT++)) || true | ||
| done | ||
| echo "Generated SBOM bundles: ${SIGNED_COUNT}" | ||
| ls "${CALLER_DIST}"/*.sbom.sigstore.json 2>/dev/null || echo "ℹ️ No SBOM bundles generated (GoReleaser may not have generated SBOMs)" | ||
| - name: Upload attestation bundles to S3 | ||
| working-directory: _workflows | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| BUCKET="${{ env.S3_BUCKET }}" | ||
| DIRECTORY="${{ steps.s3-directory.outputs.S3_DIRECTORY }}" | ||
| UPLOAD_COUNT=0 | ||
| # Upload all sigstore bundles (provenance and SBOM) | ||
| for bundle in "${CALLER_DIST}"/*.sigstore.json; do | ||
| [ -f "$bundle" ] || continue | ||
| BASENAME=$(basename "$bundle") | ||
| echo "Uploading $BASENAME to S3..." | ||
| aws s3 cp "$bundle" "s3://$BUCKET/$DIRECTORY/$BASENAME" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/json" | ||
| ((UPLOAD_COUNT++)) | ||
| done | ||
| if [ "$UPLOAD_COUNT" -eq 0 ]; then | ||
| echo "::error::No attestation bundles found to upload" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Uploaded ${UPLOAD_COUNT} attestation bundles to S3" | ||
| - name: Set up Go for workflows | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_workflows/go.mod" | ||
| cache: false | ||
| - name: Generate manifest.json | ||
| id: generate-binaries-manifest | ||
| working-directory: _workflows | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| run: | | ||
| MANIFEST_JSON=$(go run ./cmd/generate-manifest \ | ||
| -asset-dir "${CALLER_DIST}" \ | ||
| -repo-name "${{ github.event.repository.name }}" \ | ||
| -org-name "${{ github.event.repository.owner.login }}" \ | ||
| -tag "${{ inputs.tag }}" \ | ||
| -base-url "${{ env.CDN_BASE_URL }}/${{ steps.s3-directory.outputs.S3_DIRECTORY }}") | ||
| # Debug output | ||
| echo "$MANIFEST_JSON" | ||
| # Write to GITHUB_OUTPUT for job output | ||
| { | ||
| echo "binaries_manifest<<EOF" | ||
| echo "$MANIFEST_JSON" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| - name: Output checksums for merging | ||
| id: output-checksums | ||
| working-directory: _caller | ||
| run: | | ||
| # Find the checksums file generated by GoReleaser | ||
| CHECKSUMS_FILE=$(ls dist/*checksums*.txt 2>/dev/null | head -1) | ||
| if [ -z "$CHECKSUMS_FILE" ]; then | ||
| echo "::error::No checksums file found" | ||
| exit 1 | ||
| fi | ||
| echo "Found checksums file: $CHECKSUMS_FILE" | ||
| # Output checksums content for merging with Windows hashes in registry job | ||
| # Use randomized delimiter to prevent injection via filenames containing "EOF" | ||
| DELIM="CHECKSUMS_$(openssl rand -hex 8)" | ||
| { | ||
| echo "checksums<<${DELIM}" | ||
| cat "$CHECKSUMS_FILE" | ||
| echo "${DELIM}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| goreleaser-windows: | ||
| if: inputs.msi == true | ||
| needs: determine-workflows-ref | ||
| runs-on: windows-latest | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
| env: | ||
| WINDOWS_GORELEASER_DIST: ${{ runner.temp }}/go-win-dist | ||
| outputs: | ||
| windows_manifest: ${{ steps.generate-windows-manifest.outputs.windows_manifest }} | ||
| steps: | ||
| - name: Enable Git long paths | ||
| shell: pwsh | ||
| run: git config --system core.longpaths true | ||
| - name: Checkout caller repo | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _caller | ||
| repository: ${{ github.event.repository.full_name }} | ||
| ref: refs/tags/${{ inputs.tag }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: Verify caller checkout matches release tag | ||
| working-directory: _caller | ||
| shell: bash | ||
| env: | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")" | ||
| head_commit="$(git rev-parse HEAD)" | ||
| if [ "$head_commit" != "$tag_commit" ]; then | ||
| echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit" | ||
| exit 1 | ||
| fi | ||
| echo "Verified $RELEASE_TAG at $head_commit" | ||
| - name: Checkout connector workflows | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _workflows | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| persist-credentials: false | ||
| - name: Derive AWS role names | ||
| id: role-names | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| REPO_OWNER: ${{ github.event.repository.owner.login }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| run: | | ||
| bash ./scripts/derive-iam-role-name.sh \ | ||
| --prefix GHA-Artifacts- \ | ||
| --suffix "${REPO_OWNER}-${REPO_NAME}" \ | ||
| --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" | ||
| - name: Set up Go for caller | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_caller/go.mod" | ||
| cache: false | ||
| - name: Generate or locate WXS file | ||
| id: wxs | ||
| shell: pwsh | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| CUSTOM_WXS: ${{ inputs.msi_wxs_path }} | ||
| run: | | ||
| New-Item -ItemType Directory -Force -Path "_workflows/_generated" | ||
| if ($env:CUSTOM_WXS -ne "") { | ||
| # Use custom WXS from caller repo | ||
| $wxsPath = "_caller/$env:CUSTOM_WXS" | ||
| if (-not (Test-Path $wxsPath)) { | ||
| Write-Error "Custom WXS file not found: $env:CUSTOM_WXS" | ||
| exit 1 | ||
| } | ||
| Write-Host "Using custom WXS: $wxsPath" | ||
| Copy-Item $wxsPath "_workflows/_generated/app.wxs" | ||
| "wxs_path=../_workflows/_generated/app.wxs" >> $env:GITHUB_OUTPUT | ||
| } else { | ||
| # Generate WXS from default template with deterministic UpgradeCode | ||
| Write-Host "Using default WXS template" | ||
| # Generate deterministic UUID v5 from repo name using Python's standard library | ||
| # Uses URL namespace (6ba7b810-9dad-11d1-80b4-00c04fd430c8) per RFC 4122 | ||
| $upgradeCode = (python -c "import uuid; print(str(uuid.uuid5(uuid.NAMESPACE_URL, '$env:REPO_NAME')).upper())") | ||
| Write-Host "Generated UpgradeCode for $env:REPO_NAME`: $upgradeCode" | ||
| # Read template and substitute UpgradeCode | ||
| $template = Get-Content "_workflows/templates/.wxs-default-template.wxs" -Raw | ||
| $template = $template -replace '\$\{UPGRADE_CODE\}', $upgradeCode | ||
| $template | Out-File "_workflows/_generated/app.wxs" -Encoding utf8 | ||
| "wxs_path=../_workflows/_generated/app.wxs" >> $env:GITHUB_OUTPUT | ||
| } | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3 | ||
| - name: Download syft | ||
| uses: anchore/sbom-action/download-syft@v0 | ||
| - name: Generate configs for Windows | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| WXS_PATH: ${{ steps.wxs.outputs.wxs_path }} | ||
| WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | ||
| export WINDOWS_GORELEASER_DIST="${WINDOWS_GORELEASER_DIST//\\//}" | ||
| mkdir -p "$WINDOWS_GORELEASER_DIST" | ||
| # Generate GoReleaser config | ||
| envsubst < templates/.goreleaser-windows-template.yaml.tmpl | tee "_generated/.goreleaser.windows.yaml" | ||
| # Generate provenance predicate | ||
| envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "_generated/predicate.json" | ||
| - name: Run GoReleaser for Windows | ||
| uses: goreleaser/goreleaser-action@v6 | ||
| with: | ||
| distribution: goreleaser-pro | ||
| workdir: _caller | ||
| version: "~> v2.13" | ||
| args: release --clean --skip=publish --config ../_workflows/_generated/.goreleaser.windows.yaml | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} | ||
| GORELEASER_KEY: ${{ secrets.GORELEASER_PRO_KEY }} | ||
| - name: Stage Windows dist assets | ||
| shell: pwsh | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| if (Test-Path "_caller/dist") { | ||
| Remove-Item "_caller/dist" -Recurse -Force | ||
| } | ||
| New-Item -ItemType Directory -Force -Path "_caller/dist" | ||
| $zipName = "${env:REPO_NAME}-${env:RELEASE_TAG}-windows-amd64.zip" | ||
| $msiName = "${env:REPO_NAME}_${env:RELEASE_TAG}_windows_amd64.msi" | ||
| $artifacts = @( | ||
| $zipName, | ||
| "$zipName.sig", | ||
| "$zipName.cert", | ||
| $msiName, | ||
| "$msiName.sig", | ||
| "$msiName.cert" | ||
| ) | ||
| $artifacts | ForEach-Object { | ||
| $artifact = $_ | ||
| $found = Get-ChildItem -Path $env:WINDOWS_GORELEASER_DIST -Recurse -File | Where-Object { $_.Name -eq $artifact } | ||
| if ($found.Count -ne 1) { | ||
| Write-Error "Expected exactly one Windows dist output named $artifact, found $($found.Count)" | ||
| exit 1 | ||
| } | ||
| Write-Host "Copying $artifact to dist root" | ||
| Copy-Item $found[0].FullName "_caller/dist/$artifact" | ||
| } | ||
| - name: Generate Windows dist SBOMs | ||
| shell: pwsh | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| $artifacts = @( | ||
| "${env:REPO_NAME}-${env:RELEASE_TAG}-windows-amd64.zip", | ||
| "${env:REPO_NAME}_${env:RELEASE_TAG}_windows_amd64.msi" | ||
| ) | ||
| Push-Location "_caller/dist" | ||
| try { | ||
| $artifacts | ForEach-Object { | ||
| $artifact = $_ | ||
| if (!(Test-Path $artifact)) { | ||
| Write-Error "Expected Windows dist artifact was not generated: $artifact" | ||
| exit 1 | ||
| } | ||
| $sbomPath = "$artifact.sbom.json" | ||
| syft "file:$artifact" -o "spdx-json=$sbomPath" | ||
| if ($LASTEXITCODE -ne 0) { | ||
| exit $LASTEXITCODE | ||
| } | ||
| } | ||
| } finally { | ||
| Pop-Location | ||
| } | ||
| - name: Generate SLSA provenance for Windows artifacts | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| run: | | ||
| set -euo pipefail | ||
| PROVENANCE_COUNT=0 | ||
| # Find all downloadable archives (zip and msi files) | ||
| # MSI files are staged to dist root by the previous step | ||
| for artifact in "${CALLER_DIST}"/*.zip "${CALLER_DIST}"/*.msi; do | ||
| [ -f "$artifact" ] || continue | ||
| [[ "$artifact" == *checksums* ]] && continue | ||
| BASENAME=$(basename "$artifact") | ||
| echo "Generating provenance for: $BASENAME" | ||
| cosign attest-blob \ | ||
| --yes \ | ||
| --predicate "_generated/predicate.json" \ | ||
| --type slsaprovenance1 \ | ||
| --bundle "${CALLER_DIST}/${BASENAME}.provenance.sigstore.json" \ | ||
| "$artifact" > /dev/null | ||
| echo "✅ Created ${BASENAME}.provenance.sigstore.json" | ||
| ((PROVENANCE_COUNT++)) || true | ||
| done | ||
| echo "Generated provenance bundles: ${PROVENANCE_COUNT}" | ||
| if [ "$PROVENANCE_COUNT" -eq 0 ]; then | ||
| echo "::error::No provenance bundles were generated - this indicates a build problem" | ||
| exit 1 | ||
| fi | ||
| ls "${CALLER_DIST}"/*.provenance.sigstore.json | ||
| - name: Sign SBOMs as attestation bundles | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| CALLER_DIST: ../_caller/dist | ||
| run: | | ||
| set -euo pipefail | ||
| SIGNED_COUNT=0 | ||
| # Require every Windows release artifact to have an SPDX SBOM. | ||
| # MSI files are staged to dist root by the previous step. | ||
| for artifact in "${CALLER_DIST}"/*.zip "${CALLER_DIST}"/*.msi; do | ||
| [ -f "$artifact" ] || continue | ||
| [[ "$artifact" == *checksums* ]] && continue | ||
| SBOM="${artifact}.sbom.json" | ||
| if [ ! -f "$SBOM" ]; then | ||
| echo "::error::Missing SBOM for artifact: $(basename "$artifact") (expected: $SBOM)" | ||
| exit 1 | ||
| fi | ||
| echo "Signing SBOM for: $(basename "$artifact")" | ||
| cosign attest-blob \ | ||
| --yes \ | ||
| --predicate "$SBOM" \ | ||
| --type https://spdx.dev/Document \ | ||
| --bundle "${artifact}.sbom.sigstore.json" \ | ||
| "$artifact" > /dev/null | ||
| echo "✅ Created $(basename "$artifact").sbom.sigstore.json" | ||
| ((SIGNED_COUNT++)) || true | ||
| done | ||
| echo "Generated SBOM bundles: ${SIGNED_COUNT}" | ||
| if [ "$SIGNED_COUNT" -eq 0 ]; then | ||
| echo "::error::No Windows SBOM bundles were generated - this indicates a build problem" | ||
| exit 1 | ||
| fi | ||
| ls "${CALLER_DIST}"/*.sbom.sigstore.json | ||
| - name: Configure AWS credentials via OIDC | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| role-to-assume: arn:aws:iam::025044153841:role/${{ steps.role-names.outputs.gha_artifacts_role_name }} | ||
| aws-region: us-west-2 | ||
| - name: Calculate S3 directory | ||
| id: s3-directory | ||
| shell: pwsh | ||
| run: | | ||
| $org = "${{ github.event.repository.owner.login }}" | ||
| $repo = "${{ github.event.repository.name }}" | ||
| $tag = "${{ inputs.tag }}" | ||
| "S3_DIRECTORY=releases/$org/$repo/$tag" >> $env:GITHUB_OUTPUT | ||
| - name: Upload Windows artifacts to S3 | ||
| shell: pwsh | ||
| env: | ||
| S3_BUCKET: ${{ env.S3_BUCKET }} | ||
| S3_DIRECTORY: ${{ steps.s3-directory.outputs.S3_DIRECTORY }} | ||
| run: | | ||
| # Upload zip files | ||
| $zipFiles = Get-ChildItem "_caller/dist/*.zip" -ErrorAction SilentlyContinue | ||
| foreach ($zip in $zipFiles) { | ||
| Write-Host "Uploading $($zip.Name) to S3..." | ||
| aws s3 cp $zip.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($zip.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/zip" | ||
| } | ||
| # Upload MSI files staged to dist root by the earlier step | ||
| $msiFiles = Get-ChildItem "_caller/dist/*.msi" -ErrorAction SilentlyContinue | ||
| foreach ($msi in $msiFiles) { | ||
| Write-Host "Uploading $($msi.Name) to S3..." | ||
| aws s3 cp $msi.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($msi.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/x-msi" | ||
| } | ||
| # Upload signatures (.sig files) | ||
| $sigFiles = Get-ChildItem "_caller/dist/*.sig" -ErrorAction SilentlyContinue | ||
| foreach ($sig in $sigFiles) { | ||
| Write-Host "Uploading $($sig.Name) to S3..." | ||
| aws s3 cp $sig.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($sig.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/octet-stream" | ||
| } | ||
| # Upload certificates (.cert files) | ||
| $certFiles = Get-ChildItem "_caller/dist/*.cert" -ErrorAction SilentlyContinue | ||
| foreach ($cert in $certFiles) { | ||
| Write-Host "Uploading $($cert.Name) to S3..." | ||
| aws s3 cp $cert.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($cert.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/x-pem-file" | ||
| } | ||
| # Upload SBOM json files (before attestation signing) | ||
| $sbomFiles = Get-ChildItem "_caller/dist/*.sbom.json" -ErrorAction SilentlyContinue | ||
| foreach ($sbom in $sbomFiles) { | ||
| Write-Host "Uploading $($sbom.Name) to S3..." | ||
| aws s3 cp $sbom.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($sbom.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/json" | ||
| } | ||
| # Upload attestation bundles (provenance and SBOM sigstore bundles) | ||
| $bundles = Get-ChildItem "_caller/dist/*.sigstore.json" -ErrorAction SilentlyContinue | ||
| foreach ($bundle in $bundles) { | ||
| Write-Host "Uploading $($bundle.Name) to S3..." | ||
| aws s3 cp $bundle.FullName "s3://$env:S3_BUCKET/$env:S3_DIRECTORY/$($bundle.Name)" ` | ||
| --cache-control "public,max-age=31536000,immutable" ` | ||
| --content-type "application/json" | ||
| } | ||
| - name: Set up Go for workflows tools | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_workflows/go.mod" | ||
| cache: false | ||
| - name: Generate Windows manifest | ||
| id: generate-windows-manifest | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| CDN_BASE_URL: ${{ env.CDN_BASE_URL }} | ||
| S3_DIRECTORY: ${{ steps.s3-directory.outputs.S3_DIRECTORY }} | ||
| run: | | ||
| # Use Go tool for type-safe manifest generation | ||
| MANIFEST=$(go run ./cmd/generate-windows-manifest \ | ||
| -dist-dir "../_caller/dist" \ | ||
| -cdn-base-url "$CDN_BASE_URL" \ | ||
| -s3-directory "$S3_DIRECTORY") | ||
| echo "Windows manifest: $MANIFEST" | ||
| { | ||
| echo "windows_manifest<<MANIFEST_EOF" | ||
| echo "$MANIFEST" | ||
| echo "MANIFEST_EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| goreleaser-docker: | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| needs: determine-workflows-ref | ||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
| outputs: | ||
| images_manifest: ${{ steps.extract-images.outputs.images_manifest }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout caller repo | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _caller | ||
| repository: ${{ github.event.repository.full_name }} | ||
| ref: refs/tags/${{ inputs.tag }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: Verify caller checkout matches release tag | ||
| working-directory: _caller | ||
| shell: bash | ||
| env: | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")" | ||
| head_commit="$(git rev-parse HEAD)" | ||
| if [ "$head_commit" != "$tag_commit" ]; then | ||
| echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit" | ||
| exit 1 | ||
| fi | ||
| echo "Verified $RELEASE_TAG at $head_commit" | ||
| - name: Checkout connector workflows | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _workflows | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| persist-credentials: false | ||
| - name: Derive AWS role names | ||
| id: role-names | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| REPO_OWNER: ${{ github.event.repository.owner.login }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| run: | | ||
| bash ./scripts/derive-iam-role-name.sh \ | ||
| --prefix GHA-Artifacts- \ | ||
| --suffix "${REPO_OWNER}-${REPO_NAME}" \ | ||
| --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" | ||
| bash ./scripts/derive-iam-role-name.sh \ | ||
| --prefix GitHubActionsECRPushRole- \ | ||
| --suffix "${REPO_NAME}" \ | ||
| --output-name ecr_push_role_name >> "$GITHUB_OUTPUT" | ||
| - name: Set up Go for caller | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_caller/go.mod" | ||
| cache: false | ||
| - name: Install cosign | ||
| if: inputs.docker == true | ||
| uses: sigstore/cosign-installer@v3 | ||
| - name: Generate configs for Docker OCI | ||
| if: inputs.docker == true | ||
| working-directory: _workflows | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| DOCKERFILE_PATH: ../_workflows/_generated/Dockerfile | ||
| DIST_DIR: dist/oci | ||
| # For provenance predicate template | ||
| WORKFLOWS_REF: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| # Custom Dockerfile template from caller repo (if provided) | ||
| CUSTOM_DOCKERFILE_TEMPLATE: ${{ inputs.dockerfile_template }} | ||
| # Extra files to include in Docker build context (comma-separated) | ||
| DOCKER_EXTRA_FILES: ${{ inputs.docker_extra_files }} | ||
| run: | | ||
| mkdir -p "${GENERATED_DIR}" | ||
| export BUILD_STARTED_ON=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | ||
| # Generate Dockerfile from custom template or default | ||
| if [ -n "${CUSTOM_DOCKERFILE_TEMPLATE}" ]; then | ||
| CUSTOM_PATH="../_caller/${CUSTOM_DOCKERFILE_TEMPLATE}" | ||
| if [ ! -f "${CUSTOM_PATH}" ]; then | ||
| echo "::error::Custom Dockerfile template not found: ${CUSTOM_DOCKERFILE_TEMPLATE}" | ||
| exit 1 | ||
| fi | ||
| echo "Using custom Dockerfile template: ${CUSTOM_DOCKERFILE_TEMPLATE}" | ||
| envsubst '$REPO_NAME' < "${CUSTOM_PATH}" | tee "${GENERATED_DIR}/Dockerfile" | ||
| else | ||
| echo "Using default Dockerfile template" | ||
| envsubst '$REPO_NAME' < templates/.Dockerfile-template.tmpl | tee "${GENERATED_DIR}/Dockerfile" | ||
| fi | ||
| # Build EXTRA_FILES_BLOCK for goreleaser template substitution | ||
| # This will be empty string if no extra files, or the YAML block if specified | ||
| export EXTRA_FILES_BLOCK="" | ||
| if [ -n "${DOCKER_EXTRA_FILES}" ]; then | ||
| echo "Adding extra files to Docker build context: ${DOCKER_EXTRA_FILES}" | ||
| # Validate files exist and build the YAML block | ||
| EXTRA_FILES_BLOCK=" extra_files:"$'\n' | ||
| IFS=',' read -ra FILES <<< "${DOCKER_EXTRA_FILES}" | ||
| for file in "${FILES[@]}"; do | ||
| file=$(echo "$file" | xargs) # trim whitespace | ||
| if [ ! -e "../_caller/${file}" ]; then | ||
| echo "::error::Extra file/directory not found: ${file}" | ||
| exit 1 | ||
| fi | ||
| EXTRA_FILES_BLOCK="${EXTRA_FILES_BLOCK} - ${file}"$'\n' | ||
| done | ||
| export EXTRA_FILES_BLOCK | ||
| fi | ||
| # Generate goreleaser config with all substitutions | ||
| envsubst < templates/.goreleaser-docker-oci-template.yaml.tmpl | tee "${GENERATED_DIR}/.goreleaser.docker.yaml" | ||
| envsubst < templates/.slsa-provenance-predicate-template.json.tmpl | tee "${GENERATED_DIR}/predicate.json" | ||
| - name: Generate configs for Lambda | ||
| if: inputs.lambda == true | ||
| working-directory: _workflows | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| DOCKERFILE_LAMBDA_PATH: ../_workflows/_generated/Dockerfile.lambda | ||
| DIST_DIR: dist/lambda | ||
| run: | | ||
| mkdir -p "${GENERATED_DIR}" | ||
| envsubst '$REPO_NAME' < templates/.Dockerfile-lambda-template.tmpl | tee "${GENERATED_DIR}/Dockerfile.lambda" | ||
| envsubst < templates/.goreleaser-docker-lambda-template.yaml.tmpl | tee "${GENERATED_DIR}/.goreleaser.lambda.yaml" | ||
| - name: Set up QEMU | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| uses: docker/setup-qemu-action@v3 | ||
| with: | ||
| platforms: linux/arm64 | ||
| - name: Set up Docker Buildx | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| uses: docker/setup-buildx-action@v3 | ||
| - name: Configure Public ECR AWS credentials via OIDC | ||
| if: inputs.docker == true | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| role-to-assume: arn:aws:iam::025044153841:role/${{ steps.role-names.outputs.gha_artifacts_role_name }} | ||
| aws-region: us-east-1 | ||
| - name: Login to Public ECR | ||
| if: inputs.docker == true | ||
| uses: aws-actions/amazon-ecr-login@v2 | ||
| with: | ||
| registry-type: public | ||
| - name: Run GoReleaser for Docker OCI | ||
| if: inputs.docker == true | ||
| uses: goreleaser/goreleaser-action@v6 | ||
| with: | ||
| workdir: _caller | ||
| version: "~> v2.13" | ||
| args: release --clean --config ../_workflows/_generated/.goreleaser.docker.yaml | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} | ||
| COSIGN_EXPERIMENTAL: "1" | ||
| - name: Configure Lambda ECR AWS credentials via OIDC | ||
| if: inputs.lambda == true | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| role-to-assume: "arn:aws:iam::168442440833:role/${{ steps.role-names.outputs.ecr_push_role_name }}" | ||
| aws-region: us-west-2 | ||
| - name: Login to Lambda ECR | ||
| if: inputs.lambda == true | ||
| uses: aws-actions/amazon-ecr-login@v2 | ||
| - name: Run GoReleaser for Lambda | ||
| if: inputs.lambda == true | ||
| uses: goreleaser/goreleaser-action@v6 | ||
| with: | ||
| workdir: _caller | ||
| version: "~> v2.13" | ||
| args: release --clean --config ../_workflows/_generated/.goreleaser.lambda.yaml | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.RELENG_GITHUB_TOKEN }} | ||
| - name: Set up Go for workflows | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_workflows/go.mod" | ||
| cache: false | ||
| - name: Extract image digests from GoReleaser assets | ||
| id: extract-images | ||
| if: inputs.docker == true || inputs.lambda == true | ||
| working-directory: _workflows | ||
| env: | ||
| CALLER_DIST_OCI: ../_caller/dist/oci | ||
| CALLER_DIST_LAMBDA: ../_caller/dist/lambda | ||
| run: | | ||
| IMAGES_JSON=$(go run ./cmd/extract-images \ | ||
| -include-public=${{ inputs.docker }} \ | ||
| -include-lambda=${{ inputs.lambda }} \ | ||
| -asset-dir "${CALLER_DIST_OCI}" \ | ||
| -lambda-asset-dir "${CALLER_DIST_LAMBDA}" \ | ||
| -repo-name "${{ github.event.repository.name }}" \ | ||
| -tag "${{ inputs.tag }}") | ||
| # Debug output | ||
| echo "$IMAGES_JSON" | ||
| # Write to GITHUB_OUTPUT for job output | ||
| { | ||
| echo "images_manifest<<EOF" | ||
| echo "$IMAGES_JSON" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| - name: Generate SLSA provenance for images | ||
| if: inputs.docker == true | ||
| working-directory: _workflows | ||
| env: | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| CALLER_DIST_OCI: ../_caller/dist/oci | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| VERSION="${RELEASE_TAG#v}" # Remove 'v' prefix | ||
| # Read digests from the digest file | ||
| DIGEST_FILE="${CALLER_DIST_OCI}/${{ github.event.repository.name }}_${VERSION}_digests.txt" | ||
| if [ ! -f "$DIGEST_FILE" ]; then | ||
| echo "::warning::Digest file not found: $DIGEST_FILE" | ||
| exit 0 | ||
| fi | ||
| # Attest each multi-arch index image (tagged with just version, not version-arch) | ||
| while IFS= read -r line || [ -n "$line" ]; do | ||
| [ -z "$line" ] && continue | ||
| DIGEST_HEX=$(echo "$line" | awk '{print $1}') | ||
| REF=$(echo "$line" | awk '{print $2}') | ||
| # Only attest multi-arch index images (tagged with just version) | ||
| if [[ "$REF" != *":${VERSION}" ]]; then | ||
| continue | ||
| fi | ||
| # Build the digest-pinned reference | ||
| IMAGE_BASE="${REF%:*}" | ||
| URI="${IMAGE_BASE}@sha256:${DIGEST_HEX}" | ||
| echo "Attesting image: $URI" | ||
| cosign attest \ | ||
| --yes \ | ||
| --type https://slsa.dev/provenance/v1 \ | ||
| --predicate "${GENERATED_DIR}/predicate.json" \ | ||
| "$URI" | ||
| echo "✅ Attested $URI" | ||
| done < "$DIGEST_FILE" | ||
| publish-release-manifest: | ||
| # Release manifest publication: manifest + checksums + S3 upload. | ||
| # Require binaries to succeed; windows and docker may be skipped based on inputs. | ||
| # Each optional job must succeed if it ran — a failure means incomplete release artifacts. | ||
| # see: https://docs.github.com/en/actions/using-jobs/using-conditions-to-control-job-execution | ||
| if: ${{ !cancelled() && needs.goreleaser-binaries.result == 'success' && (needs.goreleaser-windows.result == 'success' || needs.goreleaser-windows.result == 'skipped') && (needs.goreleaser-docker.result == 'success' || needs.goreleaser-docker.result == 'skipped') }} | ||
| needs: [determine-workflows-ref, goreleaser-binaries, goreleaser-windows, goreleaser-docker] | ||
| outputs: | ||
| merged_manifest: ${{ steps.export-manifest.outputs.merged_manifest }} | ||
| manifest_url: ${{ steps.upload-manifest.outputs.manifest_url }} | ||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout connector workflows | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _workflows | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| persist-credentials: false | ||
| - name: Derive AWS role names | ||
| id: role-names | ||
| working-directory: _workflows | ||
| shell: bash | ||
| env: | ||
| REPO_OWNER: ${{ github.event.repository.owner.login }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| run: | | ||
| bash ./scripts/derive-iam-role-name.sh \ | ||
| --prefix GHA-Artifacts- \ | ||
| --suffix "${REPO_OWNER}-${REPO_NAME}" \ | ||
| --output-name gha_artifacts_role_name >> "$GITHUB_OUTPUT" | ||
| - name: Set up Go for workflows | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_workflows/go.mod" | ||
| cache: false | ||
| - name: Merge binaries, Windows, and images manifests | ||
| working-directory: _workflows | ||
| env: | ||
| BINARIES_MANIFEST: ${{ needs.goreleaser-binaries.outputs.binaries_manifest }} | ||
| WINDOWS_MANIFEST: ${{ needs.goreleaser-windows.outputs.windows_manifest }} | ||
| IMAGES_MANIFEST: ${{ needs.goreleaser-docker.outputs.images_manifest }} | ||
| OUTPUT_DIR: _output | ||
| run: | | ||
| mkdir -p "${OUTPUT_DIR}" | ||
| go run ./cmd/merge-manifests \ | ||
| -binaries-manifest "$BINARIES_MANIFEST" \ | ||
| -windows-manifest "$WINDOWS_MANIFEST" \ | ||
| -images-manifest "$IMAGES_MANIFEST" \ | ||
| | tee "${OUTPUT_DIR}/manifest.json" | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3 | ||
| - name: Configure AWS credentials via OIDC | ||
| uses: aws-actions/configure-aws-credentials@v5 | ||
| with: | ||
| role-to-assume: arn:aws:iam::025044153841:role/${{ steps.role-names.outputs.gha_artifacts_role_name }} | ||
| aws-region: us-west-2 | ||
| - name: Create unified checksums file | ||
| working-directory: _workflows/_output | ||
| env: | ||
| BINARIES_CHECKSUMS: ${{ needs.goreleaser-binaries.outputs.binaries_checksums }} | ||
| WINDOWS_MANIFEST: ${{ needs.goreleaser-windows.outputs.windows_manifest }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| VERSION: ${{ inputs.tag }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| # Determine checksums filename (matches GoReleaser default pattern) | ||
| VERSION_NO_V="${VERSION#v}" | ||
| CHECKSUMS_FILE="${REPO_NAME}_${VERSION_NO_V}_checksums.txt" | ||
| # Start with binaries checksums | ||
| echo "Creating unified checksums file: $CHECKSUMS_FILE" | ||
| echo "$BINARIES_CHECKSUMS" > "./${CHECKSUMS_FILE}" | ||
| # Append Windows asset hashes from manifest | ||
| # Format: <sha256> <filename> | ||
| if [ -n "$WINDOWS_MANIFEST" ] && [ "$WINDOWS_MANIFEST" != "{}" ]; then | ||
| echo "Appending Windows hashes..." | ||
| echo "$WINDOWS_MANIFEST" | jq -r 'to_entries[] | "\(.value.sha256) \(.value.filename)"' >> "./${CHECKSUMS_FILE}" | ||
| fi | ||
| echo "Unified checksums file:" | ||
| cat "./${CHECKSUMS_FILE}" | ||
| # Sign the checksums file | ||
| echo "Signing checksums file..." | ||
| cosign sign-blob --yes "./${CHECKSUMS_FILE}" \ | ||
| --output-signature "./${CHECKSUMS_FILE}.sig" \ | ||
| --output-certificate "./${CHECKSUMS_FILE}.cert" | ||
| # Generate provenance predicate | ||
| PREDICATE_FILE="checksums-predicate.json" | ||
| cat > "$PREDICATE_FILE" << EOF | ||
| { | ||
| "buildDefinition": { | ||
| "buildType": "https://github.com/ConductorOne/github-workflows/.github/workflows/release.yaml", | ||
| "externalParameters": { | ||
| "repository": "${{ github.repository }}", | ||
| "tag": "${VERSION}" | ||
| } | ||
| }, | ||
| "runDetails": { | ||
| "builder": { | ||
| "id": "https://github.com/ConductorOne/github-workflows/.github/workflows/release.yaml@${{ needs.determine-workflows-ref.outputs.ref }}" | ||
| }, | ||
| "metadata": { | ||
| "invocationId": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
| } | ||
| } | ||
| } | ||
| EOF | ||
| # Generate provenance for checksums | ||
| cosign attest-blob \ | ||
| --yes \ | ||
| --predicate "$PREDICATE_FILE" \ | ||
| --type slsaprovenance1 \ | ||
| --bundle "./${CHECKSUMS_FILE}.provenance.sigstore.json" \ | ||
| "./${CHECKSUMS_FILE}" > /dev/null | ||
| echo "✅ Checksums signed with provenance" | ||
| - name: Update manifest checksums hash | ||
| working-directory: _workflows/_output | ||
| env: | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| VERSION: ${{ inputs.tag }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| # The unified checksums file may differ from the binaries-only version | ||
| # (when Windows hashes were appended). Update the manifest to match. | ||
| VERSION_NO_V="${VERSION#v}" | ||
| CHECKSUMS_FILE="${REPO_NAME}_${VERSION_NO_V}_checksums.txt" | ||
| NEW_SHA=$(sha256sum "./${CHECKSUMS_FILE}" | awk '{print $1}') | ||
| NEW_SIZE=$(wc -c < "./${CHECKSUMS_FILE}" | tr -d ' ') | ||
| echo "Updating manifest checksums hash: ${NEW_SHA} (${NEW_SIZE} bytes)" | ||
| jq --arg sha "$NEW_SHA" --argjson size "$NEW_SIZE" \ | ||
| 'if .assets.checksums then .assets.checksums.sha256 = $sha | .assets.checksums.sizeBytes = $size else . end' \ | ||
| manifest.json > manifest.tmp && mv manifest.tmp manifest.json | ||
| - name: Sign final manifest.json | ||
| working-directory: _workflows/_output | ||
| env: { COSIGN_EXPERIMENTAL: "1" } | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| cosign sign-blob --yes "manifest.json" \ | ||
| --output-signature "manifest.json.sig" \ | ||
| --output-certificate "manifest.json.cert" \ | ||
| --bundle "manifest.json.sigstore.json" | ||
| - name: Export final manifest for registry API | ||
| id: export-manifest | ||
| working-directory: _workflows/_output | ||
| run: | | ||
| # Output the final merged+signed manifest as a job output | ||
| # so record-registry-api can use the exact same manifest. | ||
| echo "merged_manifest=$(cat manifest.json | jq -c .)" >> "$GITHUB_OUTPUT" | ||
| - name: Upload checksums to S3 | ||
| working-directory: _workflows/_output | ||
| env: | ||
| BUCKET: ${{ env.S3_BUCKET }} | ||
| DIRECTORY: ${{ needs.goreleaser-binaries.outputs.s3_directory }} | ||
| REPO_NAME: ${{ github.event.repository.name }} | ||
| VERSION: ${{ inputs.tag }} | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| VERSION_NO_V="${VERSION#v}" | ||
| CHECKSUMS_FILE="${REPO_NAME}_${VERSION_NO_V}_checksums.txt" | ||
| aws s3 cp "./${CHECKSUMS_FILE}" "s3://${BUCKET}/${DIRECTORY}/${CHECKSUMS_FILE}" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "text/plain" | ||
| aws s3 cp "./${CHECKSUMS_FILE}.sig" "s3://${BUCKET}/${DIRECTORY}/${CHECKSUMS_FILE}.sig" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/octet-stream" | ||
| aws s3 cp "./${CHECKSUMS_FILE}.cert" "s3://${BUCKET}/${DIRECTORY}/${CHECKSUMS_FILE}.cert" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/x-pem-file" | ||
| aws s3 cp "./${CHECKSUMS_FILE}.provenance.sigstore.json" "s3://${BUCKET}/${DIRECTORY}/${CHECKSUMS_FILE}.provenance.sigstore.json" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/json" | ||
| echo "✅ Checksums file and signatures uploaded" | ||
| - name: Upload manifest.json to S3 | ||
| id: upload-manifest | ||
| working-directory: _workflows/_output | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| BUCKET="${{ env.S3_BUCKET }}" | ||
| DIRECTORY="${{ needs.goreleaser-binaries.outputs.s3_directory }}" | ||
| CDN_BASE="${{ env.CDN_BASE_URL }}" | ||
| # manifest.json is required | ||
| if [ ! -f "manifest.json" ]; then | ||
| echo "::error::manifest.json not found in $(pwd)" | ||
| exit 1 | ||
| fi | ||
| if aws s3 cp "manifest.json" "s3://$BUCKET/$DIRECTORY/manifest.json" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/json"; then | ||
| MANIFEST_URL="$CDN_BASE/$DIRECTORY/manifest.json" | ||
| echo "manifest_url=$MANIFEST_URL" >> "$GITHUB_OUTPUT" | ||
| echo "✅ Manifest uploaded successfully: $MANIFEST_URL" | ||
| else | ||
| echo "::error::Failed to upload manifest.json to S3" | ||
| exit 1 | ||
| fi | ||
| for required_file in manifest.json.sig manifest.json.cert manifest.json.sigstore.json; do | ||
| if [ ! -f "$required_file" ]; then | ||
| echo "::error::$required_file not found in $(pwd)" | ||
| exit 1 | ||
| fi | ||
| done | ||
| aws s3 cp "manifest.json.sig" "s3://$BUCKET/$DIRECTORY/manifest.json.sig" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/octet-stream" | ||
| aws s3 cp "manifest.json.cert" "s3://$BUCKET/$DIRECTORY/manifest.json.cert" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/octet-stream" | ||
| aws s3 cp "manifest.json.sigstore.json" "s3://$BUCKET/$DIRECTORY/manifest.json.sigstore.json" \ | ||
| --cache-control "public,max-age=31536000,immutable" \ | ||
| --content-type "application/json" | ||
| # ================================================================ | ||
| # Registry API: record release after release manifest publication. | ||
| # This is the sole release metadata recording path. | ||
| # ================================================================ | ||
| record-registry-api: | ||
| # Use !cancelled() so the explicit needs.result check controls skipped-job behavior. | ||
| if: ${{ !cancelled() && needs.publish-release-manifest.result == 'success' && needs.verify-release.result == 'success' }} | ||
| needs: [determine-workflows-ref, publish-release-manifest, verify-release] | ||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout connector workflows | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _workflows | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| persist-credentials: false | ||
| - name: Set up Go for workflows | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version-file: "_workflows/go.mod" | ||
| cache: false | ||
| - name: Checkout connector repo | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| path: _connector | ||
| repository: ${{ github.event.repository.full_name }} | ||
| ref: refs/tags/${{ inputs.tag }} | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| - name: Verify connector checkout matches release tag | ||
| id: verify-connector-checkout | ||
| working-directory: _connector | ||
| shell: bash | ||
| env: | ||
| RELEASE_TAG: ${{ inputs.tag }} | ||
| run: | | ||
| set -euo pipefail | ||
| tag_commit="$(git rev-list -n 1 "refs/tags/$RELEASE_TAG")" | ||
| head_commit="$(git rev-parse HEAD)" | ||
| if [ "$head_commit" != "$tag_commit" ]; then | ||
| echo "::error::Checked out $head_commit but refs/tags/$RELEASE_TAG resolves to $tag_commit" | ||
| exit 1 | ||
| fi | ||
| echo "commit_sha=$head_commit" >> "$GITHUB_OUTPUT" | ||
| echo "Verified $RELEASE_TAG at $head_commit" | ||
| - name: Read connector documentation | ||
| id: read-docs | ||
| run: | | ||
| if [ -f "_connector/docs/connector.mdx" ]; then | ||
| echo "Found docs/connector.mdx" | ||
| echo "has_docs=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "No docs/connector.mdx found" | ||
| echo "has_docs=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
| - name: Get GitHub OIDC token for registry API | ||
| id: registry-oidc | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const token = await core.getIDToken('connector-registry') | ||
| core.setSecret(token) | ||
| core.setOutput('token', token) | ||
| - name: Fetch release metadata | ||
| id: release-meta | ||
| if: steps.registry-oidc.outcome == 'success' | ||
| continue-on-error: true | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| RELEASE_JSON=$(gh api "repos/${{ github.repository }}/releases/tags/${{ inputs.tag }}" 2>/dev/null || echo '{}') | ||
| echo "$RELEASE_JSON" | jq -r '.body // empty' > /tmp/changelog.md || true | ||
| # Prefer publish time; created_at can predate visibility for draft releases | ||
| # and for normal create-then-publish workflow runs. | ||
| RELEASED_AT=$(echo "$RELEASE_JSON" | jq -r '.published_at // .created_at // empty') | ||
| echo "released_at=$RELEASED_AT" >> "$GITHUB_OUTPUT" | ||
| - name: Write merged manifest from manifest publication job | ||
| working-directory: _workflows | ||
| env: | ||
| MERGED_MANIFEST: ${{ needs.publish-release-manifest.outputs.merged_manifest }} | ||
| run: | | ||
| mkdir -p _output | ||
| echo "$MERGED_MANIFEST" | jq . > _output/manifest.json | ||
| - name: Record release via registry API | ||
| if: steps.registry-oidc.outcome == 'success' | ||
| working-directory: _workflows | ||
| env: | ||
| REGISTRY_API_TOKEN: ${{ steps.registry-oidc.outputs.token }} | ||
| run: | | ||
| DOCS_FLAG="" | ||
| if [ "${{ steps.read-docs.outputs.has_docs }}" = "true" ]; then | ||
| DOCS_FLAG="-docs ../_connector/docs/connector.mdx" | ||
| fi | ||
| CHANGELOG_FLAG="" | ||
| if [ -s /tmp/changelog.md ]; then | ||
| CHANGELOG_FLAG="-changelog /tmp/changelog.md" | ||
| fi | ||
| CONFIG_SCHEMA_FLAG="" | ||
| if [ -f "../_connector/config_schema.json" ]; then | ||
| CONFIG_SCHEMA_FLAG="-config-schema ../_connector/config_schema.json" | ||
| fi | ||
| CAPABILITIES_FLAG="" | ||
| if [ -f "../_connector/baton_capabilities.json" ]; then | ||
| CAPABILITIES_FLAG="-capabilities ../_connector/baton_capabilities.json" | ||
| fi | ||
| RELEASED_AT_FLAG="" | ||
| if [ -n "${{ steps.release-meta.outputs.released_at }}" ]; then | ||
| RELEASED_AT_FLAG="-released-at ${{ steps.release-meta.outputs.released_at }}" | ||
| fi | ||
| go run ./cmd/record-release \ | ||
| -manifest _output/manifest.json \ | ||
| -manifest-url "${{ needs.publish-release-manifest.outputs.manifest_url }}" \ | ||
| -org "${{ github.event.repository.owner.login }}" \ | ||
| -name "${{ github.event.repository.name }}" \ | ||
| -version "${{ inputs.tag }}" \ | ||
| -repository-url "https://github.com/${{ github.repository }}" \ | ||
| -commit-sha "${{ steps.verify-connector-checkout.outputs.commit_sha }}" \ | ||
| -workflow-run-id "${{ github.run_id }}" \ | ||
| -registry-url "https://dist.conductorone.com" \ | ||
| $DOCS_FLAG \ | ||
| $CHANGELOG_FLAG \ | ||
| $CONFIG_SCHEMA_FLAG \ | ||
| $CAPABILITIES_FLAG \ | ||
| $RELEASED_AT_FLAG | ||
| verify-release: | ||
| # Verify release artifacts and attestations before recording the release. | ||
| # Registry-side verification is the trust boundary; this job prevents | ||
| # obviously bad artifacts from being submitted. | ||
| needs: [determine-workflows-ref, publish-release-manifest] | ||
| if: always() && needs.publish-release-manifest.result == 'success' | ||
| permissions: | ||
| id-token: write # Required for cosign verification | ||
| contents: read | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout shared workflows | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| repository: ConductorOne/github-workflows | ||
| ref: ${{ needs.determine-workflows-ref.outputs.ref }} | ||
| path: _workflows | ||
| persist-credentials: false | ||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@v3 | ||
| - name: Validate release artifacts | ||
| working-directory: _workflows | ||
| env: | ||
| ORG_REPO: ${{ github.event.repository.owner.login }}/${{ github.event.repository.name }} | ||
| VERSION: ${{ inputs.tag }} | ||
| run: | | ||
| ./scripts/validate-release-artifacts.sh "$ORG_REPO" "$VERSION" | ||
| notify-release-failure: | ||
| needs: | ||
| [ | ||
| determine-workflows-ref, | ||
| goreleaser-binaries, | ||
| goreleaser-windows, | ||
| goreleaser-docker, | ||
| publish-release-manifest, | ||
| record-registry-api, | ||
| verify-release, | ||
| ] | ||
| if: failure() | ||
| permissions: | ||
| checks: read | ||
| actions: read | ||
| contents: read | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Collect failure annotations | ||
| id: collect-ann | ||
| continue-on-error: true # never block the Datadog notification | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| REPO: ${{ github.repository }} | ||
| RUN_ID: ${{ github.run_id }} | ||
| run: | | ||
| set -euo pipefail | ||
| # Get the latest attempt number for this workflow run | ||
| run_json="$(gh api "repos/$REPO/actions/runs/$RUN_ID")" | ||
| attempt="$(echo "$run_json" | jq -r '.run_attempt')" | ||
| # List jobs for the *specific* attempt (prevents mixing earlier attempts) | ||
| jobs_json="$(gh api "repos/$REPO/actions/runs/$RUN_ID/attempts/$attempt/jobs?per_page=100" --paginate)" | ||
| # Filter to failed jobs in this attempt | ||
| failed_jobs=$(echo "$jobs_json" | jq -r '.jobs[] | select(.conclusion=="failure") | @base64') | ||
| out="" | ||
| if [ -z "$failed_jobs" ]; then | ||
| out="No failure annotations found for latest attempt (#${attempt})." | ||
| else | ||
| while IFS= read -r enc; do | ||
| j() { echo "$enc" | base64 -d | jq -r "$1"; } | ||
| name="$(j '.name')" | ||
| check_run_url="$(j '.check_run_url')" | ||
| # Resolve check run ID for this attempt and fetch its annotations | ||
| check_id="$(gh api "$check_run_url" -H "Accept: application/vnd.github+json" | jq -r '.id')" | ||
| anns="$(gh api "repos/$REPO/check-runs/$check_id/annotations?per_page=100" \ | ||
| --paginate -H "Accept: application/vnd.github+json" | \ | ||
| jq -r '.[] | select(.annotation_level=="failure") | .message' || true)" | ||
| if [ -n "$anns" ]; then | ||
| while IFS= read -r msg; do | ||
| oneline="$(printf "%s" "$msg" | tr '\n' ' ' | sed 's/ \+/ /g')" | ||
| out+="$name: $oneline"$'\n' | ||
| done <<< "$anns" | ||
| else | ||
| out+="$name: (no failure-level annotations; step may have failed before annotating)"$'\n' | ||
| fi | ||
| done <<< "$failed_jobs" | ||
| fi | ||
| # Keep Datadog payload compact (~4k limit on text) | ||
| out="$(printf "%s" "$out" | tail -c 3500)" | ||
| { | ||
| echo "annotations<<EOF" | ||
| printf '%s\n' "$out" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| - name: Send Datadog event on failure | ||
| uses: masci/datadog@v1 | ||
| with: | ||
| api-key: ${{ secrets.DATADOG_API_KEY }} | ||
| api-url: https://us3.datadoghq.com/ | ||
| events: | | ||
| - title: "Baton Connector Release Failed" | ||
| text: | | ||
| %%% | ||
| # `${{ github.event.repository.name }}`:`${{ inputs.tag }}` Release Failed | ||
| ## Error | ||
| [View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) | ||
| ```${{ steps.collect-ann.outputs.annotations }}``` | ||
| ## Details | ||
| * **Repository:** `${{ github.event.repository.full_name }}` | ||
| * **Release Tag:** `${{ inputs.tag }}` | ||
| * **Workflow:** `baton-connector-release` | ||
| * **Action Run ID:** `${{ github.run_id }}` | ||
| %%% | ||
| alert_type: "error" | ||
| host: ${{ github.repository_owner }} | ||
| tags: | ||
| - "github_repository:${{ github.event.repository.full_name }}" | ||
| - "github_release_tag:${{ inputs.tag }}" | ||
| - "github_workflow:baton-connector-release" | ||