This is an automated email from the ASF dual-hosted git repository. leerho pushed a commit to branch issue-12-latest-snapshots in repository https://gitbox.apache.org/repos/asf/datasketches-tck.git
commit e13811ab69d87254474299f59d3e1397c93fa475 Author: Lee Rhodes <[email protected]> AuthorDate: Sun Oct 4 16:06:38 2026 -0700 Make the latest snapshots from every language the default `snapshots update` now takes `all` and an optional revision. Without a revision, a source is updated to its default branch, so `snapshots update all` refreshes every source in one command. Pinning a specific branch, tag, or commit for one source remains available for reproducing a release or holding a source back. When several sources are processed, a failure in one no longer stops the others; the command reports every failed source and exits non-zero. An update whose commit is unchanged and whose output differs only in probabilistic snapshots no longer rewrites them, so repeated updates do not add binary churn. A new, non-required workflow runs `snapshots update all` every Monday and on demand, commits the result to main, and fails visibly when a source cannot be generated. The docs now describe main as the latest self-tested snapshots from every source and recommend that consumers test against it by default. Closes #12 Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .github/workflows/update-snapshots.yml | 87 ++++++++++++++++++++++++++++++++++ cmd/tck/__snapshots__/output_test.snap | 12 +++++ cmd/tck/__snapshots__/root_test.snap | 2 +- cmd/tck/output.go | 9 ++++ cmd/tck/output_test.go | 20 ++++++++ cmd/tck/root_test.go | 24 ++++++++++ cmd/tck/snapshots.go | 47 +++++++++++++----- docs/serialization-snapshots.md | 39 +++++++++++---- internal/snapshots/reconcile.go | 13 +++++ 9 files changed, 230 insertions(+), 23 deletions(-) diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml new file mode 100644 index 0000000..8ec1500 --- /dev/null +++ b/.github/workflows/update-snapshots.yml @@ -0,0 +1,87 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Publishes the latest self-tested snapshots from every source language, so +# that main always holds the newest snapshot set. This workflow is not a +# required check. + +name: Update snapshots + +on: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + update: + name: Update snapshots from every source + if: github.repository == 'apache/datasketches-tck' + runs-on: ubuntu-24.04 + timeout-minutes: 90 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - name: Setup mise + uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3 + + - name: Update snapshots + id: update + run: | + set +e + mise run tck -- snapshots update all 2>&1 | tee update.log + status=${PIPESTATUS[0]} + set -e + echo "status=${status}" >> "$GITHUB_OUTPUT" + { + echo '## Snapshot update' + echo + echo '```' + grep -E '^(Update |Summary:|Source revision:|✓|✗|Error:)' update.log || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Sources that updated successfully are published even when another + # source failed, so one broken upstream does not hold back the others. + - name: Commit updated snapshots + run: | + if [[ -z "$(git status --porcelain -- config.toml serialization)" ]]; then + echo "Snapshots are already up to date." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -- config.toml serialization + { + echo "chore: update snapshots to the latest upstream revisions" + echo + grep -E '^(Update |Summary:|Source revision:)' update.log || true + } > commit-message.txt + git commit --quiet --file commit-message.txt + git push origin HEAD:${{ github.ref_name }} + + - name: Report failed sources + if: steps.update.outputs.status != '0' + run: | + grep -E '^(✗|Error:)' update.log || true + exit 1 diff --git a/cmd/tck/__snapshots__/output_test.snap b/cmd/tck/__snapshots__/output_test.snap index 4521ec0..36a5bf0 100644 --- a/cmd/tck/__snapshots__/output_test.snap +++ b/cmd/tck/__snapshots__/output_test.snap @@ -24,3 +24,15 @@ Summary: 1 added · 1 modified (1 probabilistic) · 1 deleted ✗ serialization/go/snapshots has 2 blocking changes. --- + +[TestPrintResultReportsUnwrittenUpdate - 1] +╭────────┬───────────┬───────┬──────────────────┬─────────────────────────╮ +│ CHANGE │ STABILITY │ CHECK │ SNAPSHOT │ CONTENT (SIZE · SHA256) │ +├────────┼───────────┼───────┼──────────────────┼─────────────────────────┤ +│ M │ unstable │ allow │ probabilistic.sk │ old 12 B · aaaaaaaaaaaa │ +│ │ │ │ │ new 12 B · bbbbbbbbbbbb │ +╰────────┴───────────┴───────┴──────────────────┴─────────────────────────╯ +Summary: 0 added · 1 modified (1 probabilistic) · 0 deleted +✓ snapshots already matches its source revision; 1 probabilistic content modification was not written. + +--- diff --git a/cmd/tck/__snapshots__/root_test.snap b/cmd/tck/__snapshots__/root_test.snap index 7442f13..c578f57 100644 --- a/cmd/tck/__snapshots__/root_test.snap +++ b/cmd/tck/__snapshots__/root_test.snap @@ -9,7 +9,7 @@ Usage: Available Commands: check Check the snapshot set and stable snapshot contents sync Synchronize snapshots from config.toml - update Adopt one upstream revision and update its snapshots + update Adopt upstream revisions and update their snapshots Flags: -h, --help help for snapshots diff --git a/cmd/tck/output.go b/cmd/tck/output.go index 401e04b..4776f46 100644 --- a/cmd/tck/output.go +++ b/cmd/tck/output.go @@ -85,6 +85,15 @@ func printResult(output io.Writer, root string, mode snapshots.Mode, result snap } switch { + case (mode == snapshots.ModeUpdate || mode == snapshots.ModeSync) && !result.Written: + _, err := fmt.Fprintf( + output, + "✓ %s already matches its source revision; %d probabilistic content %s not written.\n", + target, + unstableModified, + plural(unstableModified, "modification was", "modifications were"), + ) + return err case mode == snapshots.ModeUpdate: _, err := fmt.Fprintf(output, "✓ Updated %s.\n", target) return err diff --git a/cmd/tck/output_test.go b/cmd/tck/output_test.go index 3552291..20d59d7 100644 --- a/cmd/tck/output_test.go +++ b/cmd/tck/output_test.go @@ -74,6 +74,26 @@ func TestPrintResultAllowsOnlyUnstableModifications(t *testing.T) { snaps.WithConfig(snaps.Raw()).MatchSnapshot(t, output.String()) } +func TestPrintResultReportsUnwrittenUpdate(t *testing.T) { + t.Parallel() + + before := testMetadata(12, 0xaa) + after := testMetadata(12, 0xbb) + result := snapshots.Result{ + Target: "snapshots", + PreviousRevision: "a", + Revision: "a", + Changes: []snapshots.Change{{ + Status: snapshots.ChangeModified, Stability: snapshots.Unstable, + Path: "probabilistic.sk", Before: &before, After: &after, + }}, + } + + var output bytes.Buffer + require.NoError(t, printResult(&output, ".", snapshots.ModeUpdate, result)) + snaps.WithConfig(snaps.Raw()).MatchSnapshot(t, output.String()) +} + func testMetadata(size int64, fill byte) snapshots.FileMetadata { metadata := snapshots.FileMetadata{Size: size} for index := range metadata.SHA256 { diff --git a/cmd/tck/root_test.go b/cmd/tck/root_test.go index 5741851..ce7dd05 100644 --- a/cmd/tck/root_test.go +++ b/cmd/tck/root_test.go @@ -46,3 +46,27 @@ func TestRunRejectsUnsupportedLanguage(t *testing.T) { require.Empty(t, output.String()) require.Contains(t, stderr.String(), `invalid argument "python" for "tck snapshots check"`) } + +func TestRunRejectsRevisionForAllUpdates(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + var stderr bytes.Buffer + exitCode := run(t.Context(), []string{"snapshots", "update", "all", "main"}, &output, &stderr) + + require.Equal(t, 1, exitCode) + require.Empty(t, output.String()) + require.Contains(t, stderr.String(), "a revision cannot be selected with all") +} + +func TestRunRejectsUnsupportedUpdateLanguage(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + var stderr bytes.Buffer + exitCode := run(t.Context(), []string{"snapshots", "update", "python"}, &output, &stderr) + + require.Equal(t, 1, exitCode) + require.Empty(t, output.String()) + require.Contains(t, stderr.String(), `unsupported snapshot language "python"`) +} diff --git a/cmd/tck/snapshots.go b/cmd/tck/snapshots.go index be11fc6..3379a6f 100644 --- a/cmd/tck/snapshots.go +++ b/cmd/tck/snapshots.go @@ -24,6 +24,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "github.com/apache/datasketches-tck/internal/snapshots" @@ -80,24 +81,34 @@ func newSnapshotSyncCommand() *cobra.Command { } func newSnapshotUpdateCommand() *cobra.Command { + validLanguages := append(snapshots.Languages(), "all") return &cobra.Command{ - Use: "update <cpp|go|java> <revision>", + Use: "update <cpp|go|java|all> [revision]", Short: reconcileDescription(snapshots.ModeUpdate), Long: reconcileDetails(snapshots.ModeUpdate), Args: func(command *cobra.Command, args []string) error { - if err := cobra.ExactArgs(2)(command, args); err != nil { + if err := cobra.RangeArgs(1, 2)(command, args); err != nil { return err } - for _, language := range snapshots.Languages() { - if args[0] == language { - return nil - } + if !slices.Contains(validLanguages, args[0]) { + return fmt.Errorf("unsupported snapshot language %q", args[0]) + } + if args[0] == "all" && len(args) == 2 { + return errors.New("a revision cannot be selected with all; each source is updated to its default branch") } - return fmt.Errorf("unsupported snapshot language %q", args[0]) + return nil }, - ValidArgs: snapshots.Languages(), + ValidArgs: validLanguages, RunE: func(command *cobra.Command, args []string) error { - return reconcileSnapshots(command, snapshots.ModeUpdate, []string{args[0]}, args[1]) + languages := []string{args[0]} + if args[0] == "all" { + languages = snapshots.Languages() + } + revision := snapshots.DefaultBranch + if len(args) == 2 { + revision = args[1] + } + return reconcileSnapshots(command, snapshots.ModeUpdate, languages, revision) }, } } @@ -114,6 +125,7 @@ func reconcileSnapshots( } outOfDate := false + var failed []string for index, language := range languages { if index > 0 { if _, err := fmt.Fprintln(command.OutOrStdout()); err != nil { @@ -139,13 +151,24 @@ func reconcileSnapshots( command.ErrOrStderr(), ) if err != nil { - return err + if len(languages) == 1 { + return err + } + // Keep going so one broken upstream does not hold back the others. + failed = append(failed, languageHeading(language)) + if _, printErr := fmt.Fprintf(command.ErrOrStderr(), "✗ %v\n", err); printErr != nil { + return fmt.Errorf("print snapshot failure: %w", printErr) + } + continue } if err := printResult(command.OutOrStdout(), root, mode, result); err != nil { return fmt.Errorf("print snapshot result: %w", err) } outOfDate = outOfDate || (mode == snapshots.ModeCheck && result.HasBlockingChanges()) } + if len(failed) > 0 { + return fmt.Errorf("%s snapshots failed for %s", strings.ToLower(modeHeading(mode)), strings.Join(failed, ", ")) + } if outOfDate { return errSnapshotsOutOfDate } @@ -159,7 +182,7 @@ func reconcileDescription(mode snapshots.Mode) string { case snapshots.ModeSync: return "Synchronize snapshots from config.toml" case snapshots.ModeUpdate: - return "Adopt one upstream revision and update its snapshots" + return "Adopt upstream revisions and update their snapshots" default: panic(fmt.Sprintf("unsupported snapshot mode %q", mode)) } @@ -172,7 +195,7 @@ func reconcileDetails(mode snapshots.Mode) string { case snapshots.ModeSync: return "Regenerate all snapshot directories from the repositories and commits in config.toml without changing the config." case snapshots.ModeUpdate: - return "Resolve an upstream commit, branch, or tag, record its exact commit ID in config.toml, and regenerate that source's snapshots." + return "Resolve an upstream commit, branch, or tag, record its exact commit ID in config.toml, and regenerate that source's snapshots. Without a revision, the source's default branch is used; all updates every source to its default branch. Snapshots are left untouched when the revision is unchanged and only probabilistic snapshots differ." default: panic(fmt.Sprintf("unsupported snapshot mode %q", mode)) } diff --git a/docs/serialization-snapshots.md b/docs/serialization-snapshots.md index 95b1af9..50ee001 100644 --- a/docs/serialization-snapshots.md +++ b/docs/serialization-snapshots.md @@ -19,7 +19,9 @@ The serialization corpus is a compatibility boundary between DataSketches implementations. Each directory under `serialization/<language>/snapshots` contains sketches produced by one implementation and intended to be read and validated by the others. -This repository generates snapshots from the upstream sources configured in `config.toml` instead of following the latest branch. Each source records its repository and exact commit so a checkout remains reproducible. +The `main` branch of this repository publishes the latest self-tested snapshots from every source language. Each language validates its own snapshots with its generators and round-trip tests before they reach the TCK, so a snapshot set that passes there is ready to publish. Whether each implementation can read the others' snapshots is then answered by running its cross-language tests against the latest set, not by reviewing the `.sk` files by hand. + +`config.toml` records the repository and exact commit each snapshot directory was generated from, so any TCK commit remains reproducible: ```toml [snapshot.go] @@ -27,26 +29,41 @@ repository = "https://github.com/apache/datasketches-go.git" commit = "730c0ca31e00b8becf8b70591ae8ca73954912d0" ``` +## Update to the latest snapshots + +With the [toolchain installed](#set-up-the-toolchain), update every source to its default branch: + +```shell +mise run tck -- snapshots update all +``` + +For each source, the command resolves the default branch to an exact commit ID, writes it to `config.toml`, and regenerates that source's snapshot directory. Generation happens before either file set is changed, so a source whose generation fails is left untouched; the remaining sources are still updated and the command exits with an error naming the failed sources. + +When a source's commit has not changed and only probabilistic snapshots differ, its snapshots are left as they are, so repeated updates do not add binary churn to the history. + +The [Update snapshots](#github-actions) workflow runs this command weekly and commits the result to `main`. + ## Update one source -With the [toolchain installed](#set-up-the-toolchain), adopt a different upstream branch, tag, or commit by specifying one source language and revision: +Update a single source to its default branch, or to a specific branch, tag, or commit: ```shell -mise run tck -- snapshots update go main +mise run tck -- snapshots update go +mise run tck -- snapshots update go v0.2.0 ``` -The command resolves the supplied revision to an exact commit ID, writes it to `[snapshot.go]` in `config.toml`, and regenerates `serialization/go/snapshots`. Generation happens before either file set is changed, so a generation failure leaves the repository untouched. +Pinning an older revision is useful for reproducing a release or for holding a source back while an upstream problem is fixed. If the upstream build or output layout changed, update the corresponding adapter in `internal/snapshots/<language>.go` before running the command. -Updates intentionally handle one source at a time so each upstream change can be reviewed independently. If the upstream build or output layout changed, update the corresponding adapter in `internal/snapshots/<language>.go` before running the command. +## Review an update -Review the resolved pin and corpus together: +Review the resolved pins and the corpus together: ```shell git diff --stat git diff -- config.toml serialization/go/snapshots ``` -Added and deleted files change the set of compatibility cases. Unexpected changes to deterministic files should be understood from the upstream change before they are accepted. +Most modified files are probabilistic snapshots, which are expected to change on every generation. The update report classifies each change, so the meaningful part of a review is short: added and deleted files change the set of compatibility cases, and changes to deterministic snapshots, such as a flag bit, should be understood from the upstream change. Verify that generation is reproducible at the new pin and run the repository checks: @@ -67,7 +84,7 @@ mise run tck -- snapshots check go Check mode does not modify the repository. It fails for added or deleted files and for content changes to deterministic snapshots. It reports, but allows, content changes to existing snapshots classified as probabilistic by `internal/snapshots/stability.go`. -This command answers whether the repository matches its pin; it does not determine whether the pin is the latest upstream commit. +This command answers whether the repository matches its pin; it does not determine whether the pin is the latest upstream commit. Use update mode for that. ## Synchronize configured snapshots @@ -90,7 +107,7 @@ mise run tck -- snapshots --help Mise supplies Go, CMake and CTest, Java, and Maven. Git is required for every source language, a C++ compiler is required for C++, and Make is required for Go. -Check mode accepts `cpp`, `go`, `java`, or `all`. Synchronization takes no arguments, while update requires one source language and revision. +Check mode accepts `cpp`, `go`, `java`, or `all`. Synchronization takes no arguments. Update accepts `cpp`, `go`, `java`, or `all`, and an optional revision for a single source. ## Use the corpus from an implementation @@ -98,6 +115,8 @@ An implementation consumes the `.sk` files as test fixtures. Its compatibility t This repository centralizes the fixture corpus and source-side generation. Consumer tests remain in the individual DataSketches implementation repositories. +By default, an implementation should test against the latest snapshot set on `main`. A failure then shows exactly which source language and sketch disagree, as early as possible. To keep a release or a CI run from changing underneath it, an implementation can instead pin a specific TCK commit. + ## Review policy for probabilistic snapshots Some upstream generators contain randomness, so byte-for-byte reproduction is not a valid invariant for every file. The stability policy is source-specific because upstream implementations do not always seed or exercise an algorithm in the same way. @@ -110,7 +129,7 @@ The probabilistic classification only controls byte-level comparison in this rep `.github/workflows/check.yml` runs `mise run check` for pull requests and pushes to `main`. It validates the Go implementation of the TCK tooling, but it does not run the upstream snapshot generators or modify committed snapshots. -Snapshot generation is kept out of the required workflow because it clones and builds three external projects, and because an automated update cannot decide whether an upstream compatibility change should be adopted. A pull request that updates a pin should include the generated corpus changes and record which source-language checks were run locally. +`.github/workflows/update-snapshots.yml` runs `snapshots update all` every Monday and on manual dispatch, and commits any updated pins and snapshots directly to `main`. It is not a required check. The job summary lists each source's revision change and change counts. If a source fails to generate, the other sources are still committed and the run fails so that the broken upstream is noticed. ## Implementation notes diff --git a/internal/snapshots/reconcile.go b/internal/snapshots/reconcile.go index f87a6b4..84b554c 100644 --- a/internal/snapshots/reconcile.go +++ b/internal/snapshots/reconcile.go @@ -29,6 +29,10 @@ import ( type Mode string +// DefaultBranch is the revision that selects a source repository's default +// branch: fetching HEAD from a remote resolves to the branch it points to. +const DefaultBranch = "HEAD" + const ( ModeCheck Mode = "check" ModeSync Mode = "sync" @@ -40,6 +44,8 @@ type Result struct { PreviousRevision string Revision string Changes []Change + // Written reports whether the snapshot directory was replaced. + Written bool } func (result Result) RevisionChanged() bool { @@ -138,11 +144,17 @@ func Reconcile( if err := replaceDirectory(target, generated); err != nil { return Result{}, err } + result.Written = true } return result, nil } revisionChanged := result.RevisionChanged() + if !revisionChanged && !result.HasBlockingChanges() { + // Nothing new upstream. Rewriting the probabilistic snapshots would only + // add binary churn to the history without adopting anything. + return result, nil + } if revisionChanged { if err := config.setCommit(language, resolvedRevision); err != nil { return Result{}, err @@ -169,6 +181,7 @@ func Reconcile( } return Result{}, err } + result.Written = true } return result, nil } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
