Skip to content

Build Windows MSI artifacts from short dist path #1

Build Windows MSI artifacts from short dist path

Build Windows MSI artifacts from short dist path #1

Workflow file for this run

# Reusable Release Workflow for ConductorOne Connectors

Check failure on line 1 in .github/workflows/release.yaml

View workflow run for this annotation

GitHub Actions / .github/workflows/release.yaml

Invalid workflow file

(Line: 454, Col: 32): Unrecognized named-value: 'runner'. Located at position 1 within expression: runner.temp
#
# 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"