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
14 changes: 9 additions & 5 deletions .github/actions/setup-tools/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Install the locked shared package, synchronize declared tools, and

inputs:
cache:
description: Restore and save managed tool installations; release jobs must disable this.
description: Cache dependencies and managed Cargo compilation; release jobs must disable this.
required: false
default: "true"

Expand All @@ -17,18 +17,22 @@ runs:
enable-cache: ${{ inputs.cache }}
cache-python: false

- name: Cache declared Rust and Cargo tools
- name: Cache managed Cargo compilation
if: inputs.cache == 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/rrt
key: rrt-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml', 'pyproject.toml', 'uv.lock', '.python-version') }}
restore-keys: rrt-${{ runner.os }}-${{ runner.arch }}-
path: ${{ runner.temp }}/rrt-cargo-build
key: >-
cargo-build-v1-${{ runner.os }}-${{ runner.arch }}-${{
hashFiles('rust-toolchain.toml', 'pyproject.toml', 'uv.lock', 'Cargo.lock', '.python-version')
}}

- name: Set up declared tools and development environment
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RESEARCH_REPO_TOOLS_HOME: ${{ runner.temp }}/rrt
RESEARCH_REPO_TOOLS_CARGO_BUILD_HOME: ${{ runner.temp }}/rrt-cargo-build
run: uv run --locked --managed-python --only-group tooling research-repo-tools setup

- name: Export verified managed paths
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ jobs:
with:
cache: "false"
- name: Verify default and exact Cargo packages
run: just release-package
run: just publish-check

release-evidence:
needs: [build, release-package]
Expand All @@ -93,7 +93,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
uvx --python 3.15 \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@055353c630604b1ae3438c1eaf534ba85e067a7e' \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@cd8594f658b5bf3ade063a685834565cc62e0cb3' \
python -I - <<'PY'
import os
import sys
Expand Down
188 changes: 188 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Prepare GitHub release

on:
push:
tags:
- v*

permissions:
contents: read
actions: read

# All cooperative draft writers serialize by repository/tag without cancellation.
concurrency:
group: release-draft-${{ github.repository }}-${{ github.ref_name }}
cancel-in-progress: false

jobs:
verify:
if: github.repository == 'acgetchell/la-stack'
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
commit: ${{ steps.source.outputs.commit }}
artifact-id: ${{ steps.handoff.outputs.artifact-id }}
artifact-digest: ${{ steps.handoff.outputs.artifact-digest }}
validated-attempt: ${{ github.run_attempt }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Install trusted package runner
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "0.13.7"
enable-cache: false
cache-python: false
- name: Verify tagged source and freeze native evidence and notes
id: source
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
shell: bash
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
[[ "$GITHUB_EVENT_NAME" == push && "$GITHUB_REF" == "refs/tags/$RELEASE_TAG" ]]
commit=$(git --no-pager rev-parse "$GITHUB_REF^{commit}")
[[ "$commit" == "$GITHUB_SHA" && "$(git --no-pager rev-parse HEAD)" == "$commit" ]]
git --no-pager merge-base --is-ancestor "$commit" origin/main
mkdir "$RUNNER_TEMP/handoff"
cp tooling/release-evidence.toml "$RUNNER_TEMP/handoff/evidence.toml"
runtime=(uv run --no-config --no-project --isolated --no-cache --python 3.15
--with 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@cd8594f658b5bf3ade063a685834565cc62e0cb3'
python -I -c 'import sys; from research_repo_tools.cli import main; sys.exit(main())')
"${runtime[@]}" --root "$GITHUB_WORKSPACE" release check "$RELEASE_TAG"
"${runtime[@]}" --root "$GITHUB_WORKSPACE" release evidence verify \
"$RUNNER_TEMP/handoff/evidence.toml" --checkout "$GITHUB_WORKSPACE" \
--commit "$commit" --version "${RELEASE_TAG#v}" \
--output "$RUNNER_TEMP/handoff/selection.json"
"${runtime[@]}" --root "$GITHUB_WORKSPACE" changelog notes "$RELEASE_TAG" > "$RUNNER_TEMP/handoff/notes.md"
printf 'commit=%s\n' "$commit" >> "$GITHUB_OUTPUT"
- name: Retain inert verified handoff
id: handoff
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: draft-handoff-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/handoff/*
if-no-files-found: error
retention-days: 30

prepare:
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
actions: read
contents: write
steps:
# No checkout, local action, project environment or handoff code executes here.
- name: Install trusted package runner
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
with:
version: "0.13.7"
enable-cache: false
cache-python: false
- name: Authenticate handoff and prepare the unpublished draft
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
RELEASE_COMMIT: ${{ needs.verify.outputs.commit }}
HANDOFF_ID: ${{ needs.verify.outputs.artifact-id }}
HANDOFF_SHA256: ${{ needs.verify.outputs.artifact-digest }}
VALIDATED_ATTEMPT: ${{ needs.verify.outputs.validated-attempt }}
working-directory: ${{ runner.temp }}
# Run Python from the runner's script file, avoiding actionlint #651's stdin deadlock.
shell: >-
uv run --no-config --no-project --isolated --no-cache --python 3.15
--with "research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@cd8594f658b5bf3ade063a685834565cc62e0cb3"
python -I "{0}"
run: |
import os
from functools import partial
from pathlib import Path
from research_repo_tools.files import replace_many
from research_repo_tools.release_assets import serialize_release_target
from research_repo_tools.release_drafts import prepare_release_draft
from research_repo_tools.release_interfaces import load_evidence_policy
from research_repo_tools.release_validation import api, download_artifact, read_pages

root = Path.cwd()
repository = os.environ["GITHUB_REPOSITORY"]
run = int(os.environ["GITHUB_RUN_ID"])
attempt = int(os.environ["GITHUB_RUN_ATTEMPT"])
if (repository != "acgetchell/la-stack" or attempt != 1
or os.environ["GITHUB_RUN_ATTEMPT"] != "1"
or os.environ["VALIDATED_ATTEMPT"] != str(attempt)
or os.environ["GITHUB_EVENT_NAME"] != "push"
or os.environ["GITHUB_REF"] != "refs/tags/" + os.environ["RELEASE_TAG"]
or os.environ["GITHUB_SHA"] != os.environ["RELEASE_COMMIT"]):
raise ValueError("draft preparation requires the original exact tag push")
identifier = int(os.environ["HANDOFF_ID"])
artifact = api(f"actions/artifacts/{identifier}", repository=repository)
producer = artifact.get("workflow_run", {})
if not (
type(artifact.get("id")) is int and artifact["id"] == identifier
and artifact.get("expired") is False
and artifact.get("name") == f"draft-handoff-{run}-{attempt}"
and artifact.get("digest") == "sha256:" + os.environ["HANDOFF_SHA256"]
and type(producer.get("id")) is int and producer["id"] == run
and producer.get("head_sha") == os.environ["GITHUB_SHA"]
):
raise ValueError("handoff artifact/run/digest identity differs")
current = api(f"actions/runs/{run}", repository=repository)
workflow = current.get("workflow_id")
if (type(current.get("id")) is not int or current["id"] != run
or type(workflow) is not int or workflow <= 0
or type(current.get("run_attempt")) is not int or current["run_attempt"] != attempt
or current.get("path") not in {
".github/workflows/prepare-release.yml",
".github/workflows/prepare-release.yml@" + os.environ["GITHUB_REF"],
".github/workflows/prepare-release.yml@" + os.environ["RELEASE_TAG"],
}
or current.get("event") != "push"
or current.get("head_branch") != os.environ["RELEASE_TAG"]
or current.get("head_sha") != os.environ["GITHUB_SHA"]):
raise ValueError("handoff producer attempt or event changed")
# Unfiltered history avoids the filtered workflow-run result cap.
history = read_pages(partial(api, repository=repository),
f"actions/workflows/{workflow}/runs", "workflow_runs")
same_tag, seen = [], set()
for previous in history:
if (type(previous.get("id")) is not int or previous["id"] <= 0
or type(previous.get("workflow_id")) is not int or previous["workflow_id"] != workflow
or not isinstance(previous.get("event"), str)
or not isinstance(previous.get("head_branch"), str)):
raise ValueError("workflow history identity is incomplete")
if previous["id"] in seen:
raise ValueError("workflow history contains duplicate runs")
seen.add(previous["id"])
if previous["event"] == "push" and previous["head_branch"] == os.environ["RELEASE_TAG"]:
same_tag.append(previous["id"])
if same_tag != [run]:
raise ValueError("tag has prior or unproven preparation intent; recover the original journal")
handoff = root / "handoff"
download_artifact(repository, artifact, handoff, ("evidence.toml", "selection.json", "notes.md"))
latest = api(f"actions/runs/{run}", repository=repository)
if any(type(latest.get(key)) is not type(current.get(key)) or latest.get(key) != current.get(key)
for key in ("id", "workflow_id", "run_attempt", "event", "head_branch", "head_sha", "path")):
raise ValueError("handoff producer changed during download")
target = prepare_release_draft(
root, repository, os.environ["RELEASE_TAG"], os.environ["RELEASE_COMMIT"],
policy=load_evidence_policy(handoff / "evidence.toml"),
expected=(handoff / "selection.json").read_bytes(), title=os.environ["RELEASE_TAG"],
notes=(handoff / "notes.md").read_bytes(), receipt=root / "draft-receipt.json", approve=True,
)
replace_many({root / "target.json": serialize_release_target(target)})
- name: Retain journal even after an unconfirmed creation
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: draft-receipt-${{ github.run_id }}-${{ github.run_attempt }}
path: |
${{ runner.temp }}/draft-receipt.json
${{ runner.temp }}/target.json
if-no-files-found: ignore
retention-days: 30
6 changes: 3 additions & 3 deletions .github/workflows/release-benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
type: string

concurrency:
group: release-benchmarks-${{ inputs.tag }}
group: release-draft-${{ github.repository }}-${{ inputs.tag }}
cancel-in-progress: false

jobs:
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
run: |
set -euo pipefail
uvx --python 3.15 \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@055353c630604b1ae3438c1eaf534ba85e067a7e' \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@cd8594f658b5bf3ade063a685834565cc62e0cb3' \
python -I - <<'PY'
import hashlib, json, os
from pathlib import Path
Expand Down Expand Up @@ -193,7 +193,7 @@ jobs:
run: |
set -euo pipefail
uvx --python 3.15 \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@055353c630604b1ae3438c1eaf534ba85e067a7e' \
--from 'research-repo-tools @ git+https://github.com/acgetchell/research-repo-tools@cd8594f658b5bf3ade063a685834565cc62e0cb3' \
python -I - <<'PY'
import hashlib, json, os
from pathlib import Path
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ jobs:
analyze:
name: GitHub Actions Security Analysis
runs-on: ubuntu-latest
timeout-minutes: 30
# Cold managed-tool setup took 29m23s; leave time for scanning and cache cleanup.
timeout-minutes: 45
permissions:
actions: read
contents: read
Expand Down
19 changes: 17 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,28 @@ in [Testing guidance](docs/dev/testing.md). README and guide doctest requirement
live in [Documentation guidance](docs/dev/docs.md).

Run `just ci` for core Rust, public behavior, or GitHub-equivalent validation.
It composes leaf validators directly and runs `clippy-all-targets` to match the
GitHub Clippy SARIF workflow. Unit and integration tests still run together once
It synchronizes the environment once, completes fast policy checks, then overlaps
Python consumer tests with one sequential Rust lane. Use `just --jobs 1 ci` for
serial execution on a constrained host. Keep benchmark acquisition separate from
validation so concurrent work does not affect measurements.
The Rust lane runs `clippy-all-targets` to match the GitHub Clippy SARIF workflow.
Unit and integration tests still run together once
through the release-profile `test-rust-ci` bucket, and doctests remain separate
because nextest does not execute them. The test, example, and benchmark buckets
retain their execution or compile-contract roles because ordinary compilation
does not execute Clippy lints.

Native CI retains the same `build (OS)` and `Run CI checks` identities. Ordinary
setup caches only managed Cargo compilation by OS, architecture and exact tool
declarations; exact-version tool installations are freshly verified. GitHub's
cache ref scopes separate PR entries from protected main. Release packaging and
publication keep caches disabled. The shared local build cache defaults to the
tooling home's `cargo-build/<host>/<Rust version>`; an absolute
`RESEARCH_REPO_TOOLS_CARGO_BUILD_HOME` selects separate compilation storage.
This cache does not replace project builds or validation. See the
[integration record](docs/dev/release-flow-integration.md) for graph ownership,
baseline timings and remaining native/published-package evidence.

`la-stack` intentionally has no notebook validation bucket: the repository has
no notebooks or supported Python binding surface. Add notebook tooling only
alongside an actual maintained notebook interface rather than speculatively.
Expand Down
6 changes: 5 additions & 1 deletion docs/BENCHMARKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -923,9 +923,13 @@ just bench-save-last
The durable baseline is derived from the accepted Step 1 local acquisition.
The committed comparison is `docs/performance.md`. Follow
[Releasing](RELEASING.md#step-2-publish-after-the-pr-is-merged) to verify the
final clean commit, create a draft, stage the independent records, and dispatch
final clean commit, wait for the tag-triggered draft, stage the independent records
against its authenticated captured target, and dispatch
attachment and publication. Both dispatches use the exact tag and owner-generated
selection JSON; downloaded files cannot authenticate their own assertions.
`just release-benchmarks "$TAG"` dispatches attachment using the transfer from
the retained owner record by default. `CANDIDATE_TRANSFER_FILE` is an optional
recovery override. Dispatch returns without waiting or acquiring data.

### Hosted Release Verification

Expand Down
Loading
Loading