Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ jobs:
s3_directory: ${{ steps.s3-directory.outputs.S3_DIRECTORY }}
binaries_manifest: ${{ steps.generate-binaries-manifest.outputs.binaries_manifest }}
binaries_checksums: ${{ steps.output-checksums.outputs.checksums }}
released_at: ${{ steps.release-meta.outputs.released_at }}
steps:
- name: Checkout caller repo
uses: actions/checkout@v5
Expand Down Expand Up @@ -402,6 +403,30 @@ jobs:
go-version-file: "_workflows/go.mod"
cache: false

- name: Fetch release metadata
id: release-meta
working-directory: _workflows
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/_release_metadata"
RELEASE_JSON="$GITHUB_WORKSPACE/_release_metadata/github-release.json"
CHANGELOG_FILE="$GITHUB_WORKSPACE/_release_metadata/binaries-changelog.md"
WORKFLOW_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

if ! gh api "repos/${{ github.repository }}/releases/tags/${{ inputs.tag }}" > "$RELEASE_JSON" 2>/dev/null; then
echo '{}' > "$RELEASE_JSON"
fi

go run ./cmd/release-metadata \
-repo-dir ../_caller \
-tag "${{ inputs.tag }}" \
-workflow-time "$WORKFLOW_TIME" \
-github-release-json "$RELEASE_JSON" \
-changelog-file "$CHANGELOG_FILE" \
>> "$GITHUB_OUTPUT"

- name: Generate manifest.json
id: generate-binaries-manifest
working-directory: _workflows
Expand All @@ -413,6 +438,7 @@ jobs:
-repo-name "${{ github.event.repository.name }}" \
-org-name "${{ github.event.repository.owner.login }}" \
-tag "${{ inputs.tag }}" \
-released-at "${{ steps.release-meta.outputs.released_at }}" \
-base-url "${{ env.CDN_BASE_URL }}/${{ steps.s3-directory.outputs.S3_DIRECTORY }}")

# Debug output
Expand Down Expand Up @@ -1293,8 +1319,8 @@ jobs:
# ================================================================
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]
if: ${{ !cancelled() && needs.goreleaser-binaries.result == 'success' && needs.publish-release-manifest.result == 'success' && needs.verify-release.result == 'success' }}
needs: [determine-workflows-ref, goreleaser-binaries, publish-release-manifest, verify-release]
permissions:
id-token: write
contents: read
Expand Down Expand Up @@ -1362,17 +1388,33 @@ jobs:

- name: Fetch release metadata
id: release-meta
if: steps.registry-oidc.outcome == 'success'
continue-on-error: true
working-directory: _workflows
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"
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/_release_metadata"
RELEASE_JSON="$GITHUB_WORKSPACE/_release_metadata/github-release.json"
CHANGELOG_FILE="$GITHUB_WORKSPACE/_release_metadata/changelog.md"
WORKFLOW_TIME="${{ needs.goreleaser-binaries.outputs.released_at }}"
if [ -z "$WORKFLOW_TIME" ]; then
WORKFLOW_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
fi

if ! gh api "repos/${{ github.repository }}/releases/tags/${{ inputs.tag }}" > "$RELEASE_JSON" 2>/dev/null; then
echo '{}' > "$RELEASE_JSON"
fi

go run ./cmd/release-metadata \
-repo-dir ../_connector \
-tag "${{ inputs.tag }}" \
-workflow-time "$WORKFLOW_TIME" \
-github-release-json "$RELEASE_JSON" \
-changelog-file "$CHANGELOG_FILE" \
>> "$GITHUB_OUTPUT"
# Keep the registry timestamp aligned with the signed manifest.
echo "released_at=$WORKFLOW_TIME" >> "$GITHUB_OUTPUT"
echo "released_at_source=binaries-manifest" >> "$GITHUB_OUTPUT"

- name: Write merged manifest from manifest publication job
working-directory: _workflows
Expand All @@ -1387,15 +1429,16 @@ jobs:
working-directory: _workflows
env:
REGISTRY_API_TOKEN: ${{ steps.registry-oidc.outputs.token }}
RELEASED_AT: ${{ needs.goreleaser-binaries.outputs.released_at }}
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"
if [ -s "${{ steps.release-meta.outputs.changelog_file }}" ]; then
CHANGELOG_FLAG="-changelog ${{ steps.release-meta.outputs.changelog_file }}"
fi

CONFIG_SCHEMA_FLAG=""
Expand All @@ -1409,8 +1452,8 @@ jobs:
fi

RELEASED_AT_FLAG=""
if [ -n "${{ steps.release-meta.outputs.released_at }}" ]; then
RELEASED_AT_FLAG="-released-at ${{ steps.release-meta.outputs.released_at }}"
if [ -n "$RELEASED_AT" ]; then
RELEASED_AT_FLAG="-released-at $RELEASED_AT"
fi

go run ./cmd/record-release \
Expand Down
25 changes: 18 additions & 7 deletions cmd/generate-manifest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,36 @@ import (

func main() {
var (
assetDir string
repoName string
orgName string
tag string
baseURL string
assetDir string
repoName string
orgName string
tag string
baseURL string
releasedAt string
)
flag.StringVar(&assetDir, "asset-dir", ".", "Directory containing distribution artifacts")
flag.StringVar(&repoName, "repo-name", "", "Repository name")
flag.StringVar(&orgName, "org-name", "", "Organization name")
flag.StringVar(&tag, "tag", "", "Release tag (e.g., v0.0.8)")
flag.StringVar(&baseURL, "base-url", "", "Base URL for artifact downloads")
flag.StringVar(&releasedAt, "released-at", "", "Release timestamp in RFC3339 format")
flag.Parse()

if repoName == "" || orgName == "" || tag == "" || baseURL == "" {
fmt.Fprintf(os.Stderr, "generate-manifest: error: repo-name, org-name, tag, and base-url are required\n")
os.Exit(1)
}

now := time.Now().UTC()
releaseTime := time.Now().UTC()
if strings.TrimSpace(releasedAt) != "" {
var err error
releaseTime, err = time.Parse(time.RFC3339, strings.TrimSpace(releasedAt))
if err != nil {
fmt.Fprintf(os.Stderr, "generate-manifest: error: released-at must be RFC3339: %v\n", err)
os.Exit(1)
}
releaseTime = releaseTime.UTC()
}
assets := make(map[string]*pb.Asset)

// Asset patterns: platform -> (pattern, mediaType)
Expand Down Expand Up @@ -154,7 +165,7 @@ func main() {
Name: &repoName,
Org: &orgName,
Semver: &tag,
ReleasedAt: timestamppb.New(now),
ReleasedAt: timestamppb.New(releaseTime),
Assets: assets,
SignatureHref: &signatureHref,
CertificateHref: &certificateHref,
Expand Down
215 changes: 215 additions & 0 deletions cmd/release-metadata/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
"time"
)

var semverTagPattern = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$`)

type githubRelease struct {
Body string `json:"body"`
PublishedAt string `json:"published_at"`
CreatedAt string `json:"created_at"`
}

type releaseMetadata struct {
ReleasedAt string
ReleasedAtSource string
Changelog string
ChangelogSource string
}

func main() {
var repoDir, tag, workflowTime, githubReleaseJSON, changelogFile string
flag.StringVar(&repoDir, "repo-dir", "", "Path to the checked-out release repository")
flag.StringVar(&tag, "tag", "", "Release tag")
flag.StringVar(&workflowTime, "workflow-time", "", "Workflow timestamp fallback in RFC3339 format")
flag.StringVar(&githubReleaseJSON, "github-release-json", "", "Optional GitHub Release JSON file")
flag.StringVar(&changelogFile, "changelog-file", "", "Path to write the computed changelog")
flag.Parse()

if repoDir == "" || tag == "" || workflowTime == "" || changelogFile == "" {
fmt.Fprintln(os.Stderr, "release-metadata: error: -repo-dir, -tag, -workflow-time, and -changelog-file are required")
os.Exit(2)
}

md, err := computeReleaseMetadata(repoDir, tag, workflowTime, githubReleaseJSON)
if err != nil {
fmt.Fprintf(os.Stderr, "release-metadata: error: %v\n", err)
os.Exit(1)
}
if err := os.WriteFile(changelogFile, []byte(md.Changelog), 0o600); err != nil {
fmt.Fprintf(os.Stderr, "release-metadata: error: write changelog: %v\n", err)
os.Exit(1)
}
writeOutput(os.Stdout, md, changelogFile)
}

func computeReleaseMetadata(repoDir, tag, workflowTime, githubReleaseJSON string) (releaseMetadata, error) {
workflowReleasedAt, err := normalizeRFC3339(workflowTime)
if err != nil {
return releaseMetadata{}, fmt.Errorf("workflow time: %w", err)
}

var md releaseMetadata
if githubReleaseJSON != "" {
if release, ok := readGitHubRelease(githubReleaseJSON); ok {
if strings.TrimSpace(release.Body) != "" {
md.Changelog = release.Body
md.ChangelogSource = "github-release-body"
}
if releasedAt, source := releaseTimestamp(release); releasedAt != "" {
md.ReleasedAt = releasedAt
md.ReleasedAtSource = source
}
}
}

tagMessage, taggerTime := annotatedTagMetadata(repoDir, tag)
if md.Changelog == "" && strings.TrimSpace(tagMessage) != "" {
md.Changelog = tagMessage
md.ChangelogSource = "annotated-tag-message"
}
if md.ReleasedAt == "" && taggerTime != "" {
md.ReleasedAt = taggerTime
md.ReleasedAtSource = "annotated-tagger-time"
}

if md.Changelog == "" {
changelog, source := generatedChangelog(repoDir, tag)
if strings.TrimSpace(changelog) != "" {
md.Changelog = changelog
md.ChangelogSource = source
}
}

if md.ReleasedAt == "" {
md.ReleasedAt = workflowReleasedAt
md.ReleasedAtSource = "workflow-time"
}
if md.ChangelogSource == "" {
md.ChangelogSource = "empty"
}
return md, nil
}

func readGitHubRelease(path string) (githubRelease, bool) {
data, err := os.ReadFile(path)
if err != nil || len(strings.TrimSpace(string(data))) == 0 {
return githubRelease{}, false
}
var release githubRelease
if err := json.Unmarshal(data, &release); err != nil {
return githubRelease{}, false
}
if release.Body == "" && release.PublishedAt == "" && release.CreatedAt == "" {
return githubRelease{}, false
}
return release, true
}

func releaseTimestamp(release githubRelease) (string, string) {
if ts, err := normalizeRFC3339(release.PublishedAt); err == nil && ts != "" {
return ts, "github-release-published-at"
}
if ts, err := normalizeRFC3339(release.CreatedAt); err == nil && ts != "" {
return ts, "github-release-created-at"
}
return "", ""
}

func annotatedTagMetadata(repoDir, tag string) (string, string) {
ref := "refs/tags/" + tag
tagType, err := gitOutput(repoDir, "cat-file", "-t", ref)
if err != nil || strings.TrimSpace(tagType) != "tag" {
return "", ""
}
out, err := gitOutput(repoDir, "for-each-ref", "--format=%(taggerdate:iso-strict)%00%(contents)", ref)
if err != nil {
return "", ""
}
parts := strings.SplitN(out, "\x00", 2)
if len(parts) != 2 {
return strings.TrimSpace(out), ""
}
taggerTime := ""
if ts, err := normalizeRFC3339(strings.TrimSpace(parts[0])); err == nil {
taggerTime = ts
}
return strings.TrimSpace(parts[1]), taggerTime
}

func generatedChangelog(repoDir, tag string) (string, string) {
currentCommit, err := gitOutput(repoDir, "rev-list", "-n", "1", "refs/tags/"+tag)
if err != nil {
return "", "empty"
}
tagsOut, err := gitOutput(repoDir, "tag", "--merged", strings.TrimSpace(currentCommit), "--sort=-v:refname")
if err != nil {
return "", "empty"
}

foundPrevious := false
for _, candidate := range strings.Fields(tagsOut) {
if candidate == tag || !semverTagPattern.MatchString(candidate) {
continue
}
foundPrevious = true
changelog := commitList(repoDir, candidate+".."+tag)
if strings.TrimSpace(changelog) != "" {
return changelog, "commit-list:" + candidate + ".." + tag
}
}
if !foundPrevious {
changelog := commitList(repoDir, tag)
if strings.TrimSpace(changelog) != "" {
return changelog, "commit-list:" + tag
}
}
return "", "empty"
}

func commitList(repoDir, rev string) string {
out, err := gitOutput(repoDir, "log", "--no-merges", "--format=- %s (%h)", rev)
if err != nil {
return ""
}
return strings.TrimSpace(out) + "\n"
}

func normalizeRFC3339(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", nil
}
t, err := time.Parse(time.RFC3339, value)
if err != nil {
return "", err
}
return t.UTC().Format(time.RFC3339), nil
}

func gitOutput(repoDir string, args ...string) (string, error) {
cmdArgs := append([]string{"-C", repoDir}, args...)
cmd := exec.Command("git", cmdArgs...)
out, err := cmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}

func writeOutput(out io.Writer, md releaseMetadata, changelogFile string) {
fmt.Fprintf(out, "released_at=%s\n", md.ReleasedAt)
fmt.Fprintf(out, "released_at_source=%s\n", md.ReleasedAtSource)
fmt.Fprintf(out, "changelog_file=%s\n", changelogFile)
fmt.Fprintf(out, "changelog_source=%s\n", md.ChangelogSource)
}
Loading