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
122 changes: 122 additions & 0 deletions .github/workflows/publish-crate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Publish immutable Rust library graph

on:
workflow_dispatch:
inputs:
operation:
description: Publish the immutable source release graph
type: choice
options: [automatic]
default: automatic
required: true
source_release_id:
description: Exact immutable GitHub source release ID
type: string
required: true
source_revision:
description: Full source revision bound by the release manifest
type: string
required: true
manifest_sha256:
description: Exact crate-release.json digest
type: string
required: true
schedule:
- cron: '17 * * * *'

permissions:
contents: read

env:
PYTHONDONTWRITEBYTECODE: '1'

concurrency:
group: content-toolkit-crate-publication
cancel-in-progress: false

jobs:
reconcile:
if: github.repository == 'atrinik/content-toolkit' && github.repository_id == '1327107043' && github.ref == 'refs/heads/main' && github.event_name == 'schedule'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Redispatch incomplete immutable crate graphs
env:
GH_TOKEN: ${{ github.token }}
GH_READ_TOKEN: ${{ github.token }}
run: python3 tools/crate_release.py reconcile

verify:
if: github.repository == 'atrinik/content-toolkit' && github.repository_id == '1327107043' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.operation == 'automatic'
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
actions: read
checks: read
attestations: read
env:
GH_READ_TOKEN: ${{ github.token }}
SOURCE_RELEASE_ID: ${{ inputs.source_release_id }}
SOURCE_REVISION: ${{ inputs.source_revision }}
MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Verify immutable release, complete inventory and registry state
run: >-
python3 tools/crate_release.py verify --release-id "${SOURCE_RELEASE_ID}"
--revision "${SOURCE_REVISION}" --manifest-sha256 "${MANIFEST_SHA256}"

publish:
needs: verify
if: github.repository == 'atrinik/content-toolkit' && github.repository_id == '1327107043' && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' && inputs.operation == 'automatic'
environment: crates-io-release
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: write
actions: read
checks: read
attestations: read
id-token: write
env:
GH_READ_TOKEN: ${{ github.token }}
SOURCE_RELEASE_ID: ${{ inputs.source_release_id }}
SOURCE_REVISION: ${{ inputs.source_revision }}
MANIFEST_SHA256: ${{ inputs.manifest_sha256 }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Revalidate the exact release before requesting OIDC
run: >-
python3 tools/crate_release.py verify --release-id "${SOURCE_RELEASE_ID}"
--revision "${SOURCE_REVISION}" --manifest-sha256 "${MANIFEST_SHA256}"
- id: auth
uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
- name: Upload verified archives in dependency order without builds
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
GH_INTENT_TOKEN: ${{ github.token }}
run: >-
python3 tools/crate_release.py upload --release-id "${SOURCE_RELEASE_ID}"
--revision "${SOURCE_REVISION}" --manifest-sha256 "${MANIFEST_SHA256}"
- name: Confirm complete registry checksums after success or uncertain upload
if: ${{ !cancelled() }}
run: >-
python3 tools/crate_release.py confirm --release-id "${SOURCE_RELEASE_ID}"
--revision "${SOURCE_REVISION}" --manifest-sha256 "${MANIFEST_SHA256}"
122 changes: 110 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,141 @@ on:
- '[0-9]+.[0-9]+.x'

permissions:
contents: write
issues: write
pull-requests: write
contents: read

concurrency:
group: content-toolkit-semantic-release
cancel-in-progress: false

jobs:
release:
name: Release content toolkit
package:
name: Prepare credential-free source release
runs-on: ubuntu-24.04
timeout-minutes: 40
outputs:
version: ${{ steps.plan.outputs.version }}
revision: ${{ steps.plan.outputs.revision }}
env:
CARGO_HOME: /tmp/atrinik-content-toolkit-cargo
CARGO_TARGET_DIR: /tmp/atrinik-content-toolkit-target
ATRINIK_RELEASE_BRANCH: ${{ github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.18.1
- name: Verify release branch policy
run: node scripts/release/test-release-policy.cjs
- name: Install inert commit analysis dependencies
run: >-
npm install --prefix "${RUNNER_TEMP}/crate-analysis"
--ignore-scripts --no-audit --no-fund --package-lock=false
@semantic-release/commit-analyzer@13.0.1
conventional-changelog-conventionalcommits@9.3.1 semver@7.7.3
- name: Test exact source version analysis
run: NODE_PATH="${RUNNER_TEMP}/crate-analysis/node_modules" node scripts/release/test-source-version.mjs
- name: Plan the actual source version without write credentials
id: plan
run: |
NODE_PATH="${RUNNER_TEMP}/crate-analysis/node_modules" \
node scripts/release/source-version.mjs > "${RUNNER_TEMP}/source-plan.json"
jq -r '"revision=" + .revision, "version=" + (.version // "")' \
"${RUNNER_TEMP}/source-plan.json" >> "${GITHUB_OUTPUT}"
- name: Install pinned release dependencies
if: steps.plan.outputs.version != ''
run: tools/install-ci-dependencies.sh
- name: Fetch the complete locked cross-target graph
if: steps.plan.outputs.version != ''
run: cargo fetch --locked
- name: Revalidate exact release revision
if: steps.plan.outputs.version != ''
run: tools/validate.sh
- name: Analyze commits, package, and publish
- name: Prepare every immutable source and crate asset
if: steps.plan.outputs.version != ''
env:
GH_TOKEN: ${{ github.token }}
SOURCE_VERSION: ${{ steps.plan.outputs.version }}
run: tools/package-release.sh build/release "${SOURCE_VERSION}"
- name: Retain the complete verified release payload
if: steps.plan.outputs.version != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: source-release-${{ github.sha }}
path: build/release/
if-no-files-found: error
retention-days: 7

attest:
name: Attest verified source release manifest
needs: package
if: needs.package.outputs.version != ''
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.package.outputs.revision }}
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: source-release-${{ github.sha }}
path: build/release/
- name: Verify complete inert payload before attestation
env:
ATRINIK_RELEASE_BRANCH: ${{ github.ref_name }}
EXPECTED_SOURCE_REVISION: ${{ needs.package.outputs.revision }}
EXPECTED_SOURCE_VERSION: ${{ needs.package.outputs.version }}
run: >-
python3 tools/crate_release.py finalize --source-tag "v${EXPECTED_SOURCE_VERSION}"
--archives build/release
- name: Bind manifest bytes to this exact source workflow invocation
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: build/release/crate-release.json
push-to-registry: false

release:
name: Release content toolkit
needs: [package, attest]
if: needs.package.outputs.version != ''
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
actions: write
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.package.outputs.revision }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.18.1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: source-release-${{ github.sha }}
path: build/release/
- name: Install pinned source release dependencies without lifecycle scripts
run: >-
npx --yes
--package=semantic-release@25.0.9
--package=@semantic-release/exec@7.1.0
--package=conventional-changelog-conventionalcommits@9.3.1
semantic-release
npm install --prefix "${RUNNER_TEMP}/semantic-release"
--ignore-scripts --no-audit --no-fund --package-lock=false
semantic-release@25.0.9 @semantic-release/exec@7.1.0
@semantic-release/github@12.0.6
conventional-changelog-conventionalcommits@9.3.1
- name: Reanalyze and publish only the verified prepared payload
env:
GH_TOKEN: ${{ github.token }}
GH_READ_TOKEN: ${{ github.token }}
ATRINIK_RELEASE_BRANCH: ${{ github.ref_name }}
EXPECTED_SOURCE_REVISION: ${{ needs.package.outputs.revision }}
EXPECTED_SOURCE_VERSION: ${{ needs.package.outputs.version }}
run: '"${RUNNER_TEMP}/semantic-release/node_modules/.bin/semantic-release"'
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ jobs:
with:
fetch-depth: 0

- name: Reject unchanged Cargo versions for changed crate contents
env:
BASE_REVISION: ${{ github.event.pull_request.base.sha || github.event.before }}
run: python3 tools/crate_prepare.py check-version --base "${BASE_REVISION}"

- name: Install pinned validation dependencies
run: tools/install-ci-dependencies.sh

Expand Down
40 changes: 35 additions & 5 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
{
"branches": ["+([0-9]).+([0-9]).x", "main"],
"branches": [
"+([0-9]).+([0-9]).x",
"main"
],
"tagFormat": "v${version}",
"plugins": [
["@semantic-release/commit-analyzer", {"preset": "conventionalcommits", "releaseRules": "./scripts/release/release-rules.cjs"}],
["@semantic-release/release-notes-generator", {"preset": "conventionalcommits"}],
["@semantic-release/exec", {"publishCmd": "tools/package-release.sh build/release ${nextRelease.version}"}],
["@semantic-release/github", {"assets": [{"path": "build/release/*"}]}]
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": "./scripts/release/release-rules.cjs"
}
],
"./scripts/release/crate-pending.cjs",
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "python3 tools/crate_release.py finalize --source-tag v${nextRelease.version} --archives build/release",
"successCmd": "python3 tools/crate_release.py dispatch --source-tag v${nextRelease.version}"
}
],
[
"@semantic-release/github",
{
"assets": [
{
"path": "build/release/*"
}
]
}
]
]
}
16 changes: 12 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@
repository validation and explicit conformance/corpus commands; do not route
toolkit builds or runtime through Classic code. Update the wrapper
supply-chain inventory whenever dependency inputs or validation paths change.
- Commits/PR titles use Conventional Commits and semantic-release owns versioned
crates, CLI, schemas, fixtures, and artifacts. Every handoff states exact
validation and any unavailable integration; runtime topology is not
applicable to a toolkit-only change.
- Commits/PR titles use Conventional Commits. Semantic-release owns GitHub
source, CLI, schema, fixture and artifact versions. Public library Cargo
versions advance together through reviewed manifest changes independently of
source versions; `policy/crate-release.json` is the explicit registry allowlist.
The read-only build job prepares the complete immutable release graph and the
separate publisher verifies official main-source manifest attestations and
reserves durable coordinate intents before exact-byte OIDC uploads, without
building. Only the inert signing job has source OIDC/attestation authority;
compiler jobs remain credential-free. Preserve uncertain intents for explicit
owner recovery. See
[the release contract](CRATE_RELEASE.md). Every handoff states exact validation
and unavailable integration; runtime topology is not applicable to toolkit work.
Loading
Loading