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
141 changes: 141 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: release

# Creates a GitHub Release once BOTH tag-triggered publish workflows
# (publish-pypi.yml, images.yml) have finished successfully for this exact
# tag. A Release exists here beyond what PyPI already shows because a tag
# also ships two ghcr.io images that PyPI knows nothing about — the Release
# is the one place that says "this tag = these packages + these images."
#
# Triggered by the same tag push as its two siblings, rather than by
# workflow_run watching them complete: `needs:` cannot cross workflow files,
# and this avoids depending on workflow_run's head_branch semantics for a
# tag-triggered source run, which nothing else in this repo relies on.
# Instead the job below polls the Actions API for the sibling runs at this
# exact commit until both report a conclusion — that only depends on the
# same push:tags trigger every other release workflow here already uses.
on:
push:
tags:
# Digit after v: keeps this to release tags, same reasoning continuo's
# release.yml uses. publish-pypi.yml and images.yml use the looser `v*`
# and instead guard internally with startsWith(..., 'v') checks, so
# this is deliberately the stricter of the three triggers.
- "v[0-9]*"

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
github-release:
# -test tags are a TestPyPI dry run (see publish-pypi.yml) and skip
# images.yml's publish job by design (its own if: guards on this same
# condition) — no public Release for them.
if: "!contains(github.ref_name, '-test')"
runs-on: ubuntu-latest
# Each wait_for call below can itself run up to 30 minutes (its own
# internal timeout), called twice, sequentially — so this job's ceiling
# has to clear 60 minutes on its own polling math alone, before even
# accounting for how long the siblings actually take to run.
timeout-minutes: 70
permissions:
contents: write # gh release create/edit
actions: read # poll sibling workflow runs
env:
TAG: ${{ github.ref_name }}
SHA: ${{ github.sha }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Wait for publish-pypi.yml and images.yml to finish on this commit
run: |
set -euo pipefail

wait_for() {
local workflow_file="$1" elapsed=0 interval=20 timeout=1800
while :; do
# event=push, not just head_sha: images.yml also triggers on
# pull_request, and a coincidentally-identical head_sha between
# an open PR and this tag would otherwise pick up the wrong run.
# Query params go in the URL, not via -f: gh api switches a
# request to POST the moment any -f/-F flag is present, and this
# listing endpoint only accepts GET — it 404s on POST, which
# under `set -e` would abort this script on the very first poll,
# every time. Verified against this repo's real API: -f flags
# 404 here, the URL query string form returns real run data.
conclusion="$(gh api "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow_file}/runs?head_sha=${SHA}&event=push" \
--jq '.workflow_runs[0].conclusion // "pending"')"
case "${conclusion}" in
success)
echo "${workflow_file}: success"
return 0
;;
pending)
# Covers both "no matching run yet" (API eventual
# consistency right after the push) and "run exists but
# still queued/in_progress" — conclusion is JSON null in
# both cases until the run completes.
;;
*)
echo "::error::${workflow_file} concluded '${conclusion}' for ${SHA}; not creating a Release."
exit 1
;;
esac
elapsed=$((elapsed + interval))
if [ "${elapsed}" -ge "${timeout}" ]; then
echo "::error::timed out after ${timeout}s waiting for ${workflow_file} to finish on ${SHA}."
exit 1
fi
sleep "${interval}"
done
}

wait_for "publish-pypi.yml"
wait_for "images.yml"

- name: Extract this version's CHANGELOG section
id: notes
run: |
set -euo pipefail
VERSION="${TAG#v}"
# Print the body between "## [X.Y.Z]" and the next "## [" heading.
if grep -qE "^## \[${VERSION}\]" CHANGELOG.md; then
awk -v ver="## [${VERSION}]" '
index($0, ver) == 1 { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > /tmp/release-body.md
fi

if [ -s /tmp/release-body.md ]; then
echo "generate=false" >> "$GITHUB_OUTPUT"
else
# A tag with no matching CHANGELOG.md section (e.g. someone
# forgot to add one before tagging) still gets a Release — just
# with GitHub's generated notes instead of failing the job over
# a missing doc update.
echo "::warning::no '## [${VERSION}]' section in CHANGELOG.md; falling back to generated notes."
echo "generate=true" >> "$GITHUB_OUTPUT"
fi

- name: Create the Release
run: |
set -euo pipefail
# Idempotent: re-running this workflow for an existing tag updates
# the Release rather than failing.
if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
if [ "${{ steps.notes.outputs.generate }}" = "true" ]; then
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" --latest
else
gh release edit "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--notes-file /tmp/release-body.md --latest
fi
elif [ "${{ steps.notes.outputs.generate }}" = "true" ]; then
gh release create "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--title "${TAG}" --generate-notes --latest
else
gh release create "${TAG}" --repo "${GITHUB_REPOSITORY}" \
--title "${TAG}" --notes-file /tmp/release-body.md --latest
fi
84 changes: 84 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Changelog

All notable changes to this project are documented here. Format loosely
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

## [0.3.1] - 2026-08-21

### Added

- Apache License 2.0, `CODE_OF_CONDUCT.md`, `DCO` with CI-enforced sign-off,
`CONTRIBUTING.md`, `SECURITY.md`, and a gitleaks + Trivy security-scanning
pipeline, ahead of open-sourcing this repository.
- `.github/workflows/release.yml`: creates a GitHub Release for a version tag
once that tag's PyPI publish and both engine images finish successfully —
the one place documenting "this tag = these packages + these images",
since PyPI's release history says nothing about the ghcr.io images.

### Changed

- `continuo-engine-contract` 0.7.0 → 0.7.1: the published wheel now embeds
`LICENSE`/`NOTICE` and declares license metadata, which the 0.7.0 wheel
omitted. Every exact pin on it — root `pyproject.toml` and both adapters'
— updated to match.

## [0.3.0] - 2026-08-20

### Added

- `continuo-engine-contract` is now vendored in this repository as a uv
workspace member (renamed from `continuo-validation-contract`), replacing
the external PyPI dependency of the same content.
- Ported the `validation-op` CLI path and its test suite in from
continuo-validation.

### Removed

- The external `continuo-validation-contract` PyPI dependency, and every
`continuo_validation_contract` reference across the codebase.

## [0.2.1] - 2026-08-10

### Changed

- Contract pin bumped to 0.6.0; `ensure_table` aligned with the port's
`config` parameter.

### Fixed

- Swept the remaining `contract==0.4.0` pin sites; added a guard against
future pin drift.

### Added

- CI publishes the runtime base images for both amd64 and arm64.

## [0.2.0] - 2026-08-08

### Added

- Three-part content hash, replacing the earlier single-hash formula.
- Physical-layout `config` on the node contract (partitioning, sort order,
format).
- A static in-repo import-closure resolver, and a lint rule rejecting
dynamic-import constructs.

### Changed

- Adopted continuo-validation-contract 0.4.0's type grammar and read gate.

### Fixed

- Closure resolver correctness and `sys.path` handling: index-name
uniqueness under truncation, cyclic `config` detection, UTF-8-BOM
decoding, unconditional `sys.path` repositioning.

## [0.1.0] - 2026-08-03

### Added

- Initial release: the runtime harness (`conform()`, `RunContext`, the
closure resolver), the `continuo-runtime` CLI, and the Postgres/Trino
engine adapters.
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ scripts/security-scan.sh

## Conventions

- **Changelog.** A pull request whose changes are worth a release note adds an entry
under `## [Unreleased]` in [CHANGELOG.md](CHANGELOG.md), Keep a Changelog style. At
release time that section is renamed to the new version and a fresh empty
`## [Unreleased]` goes above it — `.github/workflows/release.yml` reads the section
matching the pushed tag to build the GitHub Release notes, and falls back to
GitHub's generated notes if none exists.
- **Python logging.** Use the standard `logging` module for diagnostic output, never
`print`. The only exception is machine-parsed stdout protocols (e.g. the CLI's
sentinel-framed result blocks) — those stay as explicit `print`, since stdout is
Expand Down
5 changes: 4 additions & 1 deletion tests/test_no_legacy_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
# plan and spec, and the 2026-08-07 config-hash plan). They describe what was
# true on the day they were written; rewriting them to today's names would
# falsify the design history, so they are exempt from the sweep.
EXEMPT_PREFIXES = ("docs/superpowers/",)
#
# CHANGELOG.md is the same category: its 0.3.0 entry names the package this
# rename replaced, because that is what actually shipped in that release.
EXEMPT_PREFIXES = ("docs/superpowers/", "CHANGELOG.md")


def test_no_legacy_validation_names_anywhere():
Expand Down
Loading