feat(releases): publish immutable standards bundles #8

Merged
KofTwentyTwo merged 2 commits from feat/standards-release-0-1 into main 2026-10-10 13:57:33 +00:00
19 changed files with 878 additions and 2 deletions

No files matched your search

+37
View File
@@ -4,6 +4,7 @@
name: ci
on:
workflow_call:
pull_request:
push:
branches: [main]
@@ -92,7 +93,43 @@ jobs:
shell: pwsh
run: ./tools/Test-RepoConformance.ps1 -SelfTest
- name: Standards package tests
shell: pwsh
run: ./tools/Test-StandardsPackage.ps1
# Static mode reads only the checkout: no API calls, no admin scope needed.
- name: Static conformance of this repository
shell: pwsh
run: ./tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
release-please:
name: release-please
needs: [security, codeql, docs, conformance]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.RELEASE_APP_CLIENT_ID != ''
runs-on: ubuntu-24.04
timeout-minutes: 10
# EX-0006: a main-only bot key, never a repository secret.
environment: release-automation
concurrency:
group: standards-release-pr
cancel-in-progress: false
permissions: {}
steps:
# An App token lets the generated PR trigger CI; GITHUB_TOKEN does not.
- name: Release PR token
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
- name: Propose a semantic release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
with:
token: ${{ steps.app.outputs.token }}
target-branch: main
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
skip-github-release: true
+175
View File
@@ -0,0 +1,175 @@
name: release-standards
on:
workflow_call:
permissions: {}
jobs:
validate:
name: validate
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
issues: read
pull-requests: read
outputs:
version: ${{ steps.tag.outputs.version }}
checklist: ${{ steps.tag.outputs.checklist }}
pr: ${{ steps.tag.outputs.pr }}
steps:
- name: Checkout tagged source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Validate version, main ancestry, and checklist
id: tag
env:
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
[[ "$GITHUB_REF" == refs/tags/v* ]]
[[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || {
echo '::error::Standards bundles require a stable vMAJOR.MINOR.PATCH tag.'
exit 1
}
git merge-base --is-ancestor HEAD origin/main
version="${TAG#v}"
[[ "$(cat version.txt)" == "$version" ]]
[[ "$(jq -r '."."' .release-please-manifest.json)" == "$version" ]]
echo "version=$version" >> "$GITHUB_OUTPUT"
commit="$(git rev-parse HEAD)"
pr="$(gh api "repos/$GH_REPO/commits/$commit/pulls" | jq -r --arg sha "$commit" '.[] | select(.merge_commit_sha == $sha and .merged_at != null and .base.ref == "main" and any(.labels[]; .name == "autorelease: pending")) | .number')"
[[ "$pr" =~ ^[1-9][0-9]*$ ]] || {
echo '::error::Tag the merge commit of the pending release PR.'
exit 1
}
echo "pr=$pr" >> "$GITHUB_OUTPUT"
# Preflight is approved before tagging; shipping checks follow publication.
checklist=''
if [[ "$version" == *.0 ]]; then
annotation="$(git for-each-ref --format='%(contents)' "refs/tags/$TAG")"
prefix="https://github.com/$GH_REPO/issues/"
checklist="$(printf '%s\n' "$annotation" | grep -F 'Release checklist: ' | head -n 1 | sed 's/^Release checklist: //')"
[[ "$checklist" == "$prefix"* ]]
issue="${checklist#"$prefix"}"
[[ "$issue" =~ ^[1-9][0-9]*$ ]]
gh api "repos/$GH_REPO/issues/$issue" | jq -e \
'.pull_request == null and any(.labels[]; .name == "release: ready")' >/dev/null
fi
echo "checklist=$checklist" >> "$GITHUB_OUTPUT"
gates:
name: gates
needs: validate
uses: ./.github/workflows/ci.yml
permissions:
contents: read
security-events: write
build:
name: build
needs: [validate, gates]
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Checkout tagged source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build committed standards bundle
shell: pwsh
env:
K22_VERSION: ${{ needs.validate.outputs.version }}
RELEASE_CHECKLIST: ${{ needs.validate.outputs.checklist }}
run: ./tools/Build-StandardsPackage.ps1 -Version $env:K22_VERSION -ChecklistUrl $env:RELEASE_CHECKLIST
- name: Attest every release asset
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: artifacts/standards/*
- name: Upload release assets
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: standards-release-assets
path: artifacts/standards/*
if-no-files-found: error
publish:
name: publish
needs: [validate, build]
runs-on: ubuntu-24.04
timeout-minutes: 15
environment: release
permissions:
contents: write
pull-requests: write
steps:
- name: Download release assets
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: standards-release-assets
path: release
- name: Settings read token
id: app
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.RELEASE_APP_CLIENT_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
permission-administration: read
- name: Require immutable releases
env:
GH_TOKEN: ${{ steps.app.outputs.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
[[ "$(gh api "repos/$GH_REPO/immutable-releases" --jq '.enabled')" == true ]]
- name: Create draft with all assets
working-directory: release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
K22_VERSION: ${{ needs.validate.outputs.version }}
run: |
set -euo pipefail
sha256sum --check SHA256SUMS
# Creation fails if this tag already has a release. Never replace assets.
gh release create "$TAG" --draft --verify-tag \
--title "KofTwentyTwo standards $K22_VERSION" \
--notes-file "KofTwentyTwo-standards-$K22_VERSION.notes.md" ./*
- name: Publish immutable release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
gh release edit "$TAG" --draft=false
[[ "$(gh api "repos/$GH_REPO/releases/tags/$TAG" --jq '.immutable')" == true ]]
# Release Please will not propose another version while a merged PR is pending.
- name: Mark the release PR as published
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ needs.validate.outputs.pr }}
run: |
set -euo pipefail
gh label create 'autorelease: tagged' --color 0e8a16 --force
gh pr edit "$PR_NUMBER" --remove-label 'autorelease: pending' --add-label 'autorelease: tagged'
+24
View File
@@ -0,0 +1,24 @@
name: standards-release
on:
push:
tags: ['v*']
workflow_dispatch:
permissions: {}
concurrency:
group: standards-release-${{ github.ref }}
cancel-in-progress: false
jobs:
release:
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/release-standards.yml
permissions:
contents: write
issues: read
pull-requests: write
security-events: write
id-token: write
attestations: write
+1
View File
@@ -0,0 +1 @@
{}
+65
View File
@@ -0,0 +1,65 @@
# Repository Guidelines
## Project Structure & Module Organization
This repository publishes KofTwentyTwo engineering standards and enforcement tooling.
`policies/` defines requirements; `standards/` provides implementation and language
profiles; `architecture/` contains reference designs. `configs/` holds IDE, formatter,
linter, and scanner settings. `templates/` contains starter repository files and
workflow callers; `.github/workflows/` contains reusable workflows and repository CI.
PowerShell tooling lives in `tools/`, workstation setup in `setup/`, framework
crosswalks in `compliance/`, and approved deviations in `exceptions/`.
## Build, Test, and Development Commands
There is no application build or local server. Run these checks from the repository
root with the corresponding tools installed:
```powershell
npx markdownlint-cli2 "**/*.md"
lychee --offline --include-fragments --no-progress './**/*.md'
pwsh tools/Test-RepoConformance.ps1 -SelfTest
pwsh tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
zizmor .github/workflows
actionlint
gitleaks detect --source . --no-banner
```
These check Markdown, internal links and anchors, checker behavior, local conformance,
workflow security, workflow syntax, and secrets respectively. For GitHub settings,
run the checker with `-Repository KofTwentyTwo/standards -LocalPath .`; this requires
authenticated GitHub access and sufficient permissions.
## Coding Style & Naming Conventions
Follow `.editorconfig`: UTF-8, LF, final newline, three-space indentation by default,
and two spaces for YAML. Follow language profiles in `standards/coding/` and their
matching `configs/`; PowerShell uses approved verbs and Allman braces. Markdown
follows `.markdownlint-cli2.yaml`, which disables line-length limits. Use descriptive
kebab-case document names, such as `security-program.md`.
Write requirements as `K22-<AREA>-<NN>` with MUST, SHOULD, or MAY, followed by
*Why* and *Verified by*. Never reuse or renumber IDs. Extend enforcement for new MUSTs
and update applicable compliance crosswalks in the same change.
## Testing Guidelines
Checker regression tests are built into `-SelfTest`; there is no separate test
directory or repository-wide coverage gate. Extend those tests when changing checker
behavior. Validate documentation with Markdown and internal-link checks; validate
workflow changes with actionlint and zizmor. All required CI checks must pass.
## Commit & Pull Request Guidelines
History uses Conventional Commits, such as `feat(ci): ...` and `fix(docs): ...`.
Use topic branches like `docs/clarify-dco`; target `main`. Sign commits and add DCO
sign-off with `git commit -s`. PR titles must also be Conventional Commits. Describe
the change and validation, link relevant issues, and resolve review threads before
squash merging. Discuss requirement changes in an issue first; label incompatible
new MUSTs `breaking`.
## Security & Agent Instructions
Keep content original; never copy employer or client documents. Pin external Actions
to full commit SHAs with version comments and images to digests. Follow `SECURITY.md`
for private reports and `policies/ai-assisted-development.md` for agent permissions.
+3
View File
@@ -0,0 +1,3 @@
# Changelog
Release Please maintains this file from Conventional Commits.
+16
View File
@@ -35,6 +35,22 @@ themselves: everything below comes from the [SDLC policy](policies/sdlc.md).
## Running the checks locally
Documentation and package checks:
```powershell
npx markdownlint-cli2 "**/*.md"
lychee --offline --include-fragments --no-progress './**/*.md'
pwsh tools/Test-RepoConformance.ps1 -SelfTest
pwsh tools/Test-StandardsPackage.ps1
pwsh tools/Test-RepoConformance.ps1 -LocalPath . -StaticOnly
```
Package tests use an isolated temporary Git fixture; they do not commit changes in
this repository. For version proposals and release bundles, see
[bundle releases](docs/releases.md).
Workflow and security checks:
```powershell
zizmor .github/workflows
actionlint
+7 -1
View File
@@ -11,7 +11,7 @@ reusable GitHub Actions workflows, repository templates, and a conformance check
## Status
> **Draft, before the first release (`v1.0.0`).** The requirements are written and the
> **Draft standards, before `v1.0.0`.** The requirements are written and the
> enforcement tooling exists, but no KofTwentyTwo repository has been verified against
> them yet; adoption starts with this repository, then
> [AppKit](https://github.com/KofTwentyTwo/AppKit) and
@@ -303,6 +303,12 @@ the shared release workflow ([release standard](standards/releases.md)).
## Staying current
Download a versioned ZIP or tar.gz standards bundle from
[GitHub Releases](https://github.com/KofTwentyTwo/standards/releases). Bundles preserve
the repository layout, include checksums and provenance attestations, and become
immutable on publication. See [bundle releases](docs/releases.md) for semantic version
proposals, maintainer setup, publishing, verification, and local reproduction.
- This repository is versioned with [Semantic Versioning](https://semver.org). A new
**MUST** that existing repositories do not meet is a MAJOR release; read its release
notes before updating.
+1 -1
View File
@@ -17,7 +17,7 @@ conformance check that passes when it should fail.
| Fix released | Critical 7 days, High 30 days, Medium 90 days, Low next release, from confirmation |
| Public disclosure | coordinated with the reporter; by default 90 days after the report, sooner once fixed |
Until this repository's first release (`v1.0.0`) these timeframes are targets the
Before `v1.0.0` these timeframes are targets the
maintainer works to; from `v1.0.0` they are commitments.
Fixed vulnerabilities are published as GitHub Security Advisories with a CVE. These
@@ -0,0 +1,47 @@
# ADR-0001: Release standards as immutable GitHub bundles
- **Status:** Accepted with the reviewed implementation PR
- **Date:** 2026-10-10
- **Decider:** Repository maintainer
## Context
Consumers need a versioned package of the policies, standards, configs, templates,
and tooling. Content spans several languages and two primary licenses; it has no
application runtime or build. Existing policies require Conventional Commits,
maintainer-created protected tags, gated reusable release workflows, checksums,
attestations, and immutable GitHub Releases.
## Options considered
- **GitHub ZIP/tar.gz bundle:** preserves the full repository layout and works for
every consumer with no package-manager dependency. Extraction and copying configs
remain explicit consumer actions.
- **npm or NuGet package:** convenient for one ecosystem, but imposes that ecosystem
on documentation and polyglot tooling. Adds a registry and publishing policy.
- **Automatic tags on every merge:** fewer maintainer steps, but conflicts with the
current tag-creation ruleset and release-checklist process.
## Decision
Use Release Please for version/changelog PRs and publish ZIP/tar.gz bundles through
the reusable `release-standards.yml` workflow when the maintainer tags the merged
release PR. Build from the Git commit, attest all assets, upload everything to a
draft, and publish only with repository immutability enabled.
## Consequences
- Consumers can pin, verify, and extract a standards snapshot across platforms.
- Hidden configuration and license notices remain part of the package.
- Version proposals pass normal review; maintainers retain tag creation and release
approval. No ruleset bypass or registry publishing secret is added.
- A scoped GitHub App is needed for release PR CI and the settings-read check. Its
key is held in environment secrets; installation tokens are scoped per job and
revoked. Main-only access for the version bot is covered by
[EX-0006](../../exceptions/register.md#ex-0006); publishing remains tag-only.
- Packaging excludes local edits and untracked files. Rebuilds require the same
source commit, version, checklist URL, and compatible Git tooling.
- Complete App/environment setup and the first-release checklist before activating
the pipeline. This ADR is reviewed with the implementation PR.
- Review the [distribution threat model](../security/threat-model.md), including the
version-bot credential boundary and its scoped exception, in the same PR.
+128
View File
@@ -0,0 +1,128 @@
# Standards bundle releases
The official distribution channel is
[GitHub Releases](https://github.com/KofTwentyTwo/standards/releases). Each stable
`vMAJOR.MINOR.PATCH` release contains ZIP and tar.gz archives of the **committed
repository**, with the directory layout intact, including hidden configurations,
licenses, workflows, templates, and tooling. Git metadata, untracked files, and
uncommitted edits are excluded. Extract either archive to use the standards offline
or copy its configs and templates into another repository.
## Semantic versions
[Release Please](https://github.com/googleapis/release-please) opens and updates a
release PR after successful `main` CI, changing `version.txt`, `CHANGELOG.md`, and
`.release-please-manifest.json`. The first proposal is `0.1.0`; `0.0.0` is the
unreleased bootstrap placeholder. The tag is the published version's authority; the files are checked
against it before packaging.
| Commit or squash PR title | Bump | Example |
| --- | --- | --- |
| `fix:`, `docs:`, `security:` | PATCH | `docs(testing): clarify coverage calculation` |
| `feat:` | MINOR | `feat(coding): add a language profile` |
| Any type with `!` or `BREAKING CHANGE:` | MAJOR | `feat(repository)!: require a new control` |
Use `feat` for new guidance and `!` for new obligations or removed allowances as
defined in [the versioning policy](../policies/README.md#versioning-of-the-standards).
Infrastructure changes appear in release notes and can produce PATCH releases;
`test`, `style`, and `chore` alone do not produce release notes. Review the proposed
version and notes before merging. A `breaking` label alone does not change the version.
## One-time maintainer setup
1. Keep **Settings → General → Releases → Enable release immutability** enabled.
The publisher checks the setting before creating a release and refuses to proceed
if it is disabled or unreadable. GitHub locks assets and the tag on publication;
immutability applies to future releases, not retroactively.
2. Keep [EX-0006](../exceptions/register.md#ex-0006) open and unexpired before
enabling version proposals. `K22-CI-16` otherwise restricts remaining secrets to
tag-only environments; the release-PR bot needs a narrow main-only exception.
Approval is recorded in the reviewed release-bundle implementation PR.
3. Create a GitHub App installed **only on this repository**, with Contents and Pull
requests **write**, Administration **read**, and no ruleset bypass. Store its client
ID in the Actions variable `RELEASE_APP_CLIENT_ID`. Short-lived App tokens let
release PRs trigger CI; a PR made using `GITHUB_TOKEN` would not.
4. Configure `release-automation` with deployment access for `main` only, and
`release` with a maintainer reviewer and deployment access for `v*` tags only.
Store `RELEASE_APP_PRIVATE_KEY` separately in these **environment secrets**;
never as a repository secret or committed file. Environments in the reusable
publisher resolve their own secrets; caller workflows do not pass the key.
Keep `protect-main` and `protect-release-tags` intact; the App does not create
tags or bypass review. Allow the SHA-pinned Release Please action if repository
action restrictions require an explicit allowlist entry.
No npm/NuGet publishing credential or registry package is required. Upstream App-token
and Release Please actions are pinned and tracked by Dependabot. See the
[distribution ADR](adr/0001-release-standards-bundles.md).
## Publish a version
1. Review the release PR, complete its CI and review gates, and squash-merge it.
2. For MINOR and MAJOR releases, complete the pre-tag checks in an issue made from
[the release checklist](../templates/release-checklist.md). Attach the full
conformance result and security assessment. PATCH releases still need conformance
and scan results. Keep the README's draft/adoption status accurate before `v1.0.0`.
Apply the maintainer-only `release: ready` label after sections 1–4 and the
green-main/release-notes check in section 5 are complete. Leave publication checks
unchecked until verified, then close the issue after shipping.
3. Tag the **release PR's merge commit** with its proposed version. Only the maintainer
can create protected tags. For a MINOR or MAJOR, the annotation must contain
`Release checklist: https://github.com/KofTwentyTwo/standards/issues/<number>`.
```bash
git fetch origin main
git tag -s v0.1.0 <release-pr-merge-sha> \
-m "Release checklist: https://github.com/KofTwentyTwo/standards/issues/123"
git push origin v0.1.0
```
4. The tag workflow validates ancestry, version files, the pending release PR, and
the checklist; reruns all repository CI gates; builds and attests the assets;
and waits for the `release` environment approval. Publication creates a draft with
all assets and notes, then publishes it. The release PR is marked
`autorelease: tagged` so the next version can be proposed.
These bundles ship no compiled software, so the compiled-artifact SBOM requirement
does not apply. The included workflows/configs retain their upstream pins; source
provenance and checksums identify the published bundle.
## Verify and reproduce
Every release includes `KofTwentyTwo-standards-<version>.zip`, `.tar.gz`,
`.manifest.json`, `.notes.md`, and `SHA256SUMS`. The manifest records the exact commit
and license split. Download assets from a specific version, then verify:
```bash
gh release download v0.1.0 --repo KofTwentyTwo/standards --dir standards-0.1.0
cd standards-0.1.0
sha256sum --check SHA256SUMS
gh attestation verify KofTwentyTwo-standards-0.1.0.zip --repo KofTwentyTwo/standards
```
On Windows, compare `Get-FileHash -Algorithm SHA256` to the corresponding entry in
`SHA256SUMS`. Pin reusable workflow callers to the release's full commit SHA, with a
version comment; do not use a moving branch or tag as a workflow pin.
To reproduce from a checkout containing the release commit:
```powershell
pwsh tools/Build-StandardsPackage.ps1 -Version 0.1.0 -Ref v0.1.0 -OutputPath artifacts/rebuild-0.1.0
pwsh tools/Test-StandardsPackage.ps1
```
Use the original checklist URL with `-ChecklistUrl` to reproduce `.notes.md` and
`SHA256SUMS` exactly. ZIP and tar.gz output is deterministic for a commit using the
same Git/archive tool versions. The builder refuses an existing output directory.
## Failed releases
Never replace published assets or move/reuse a version tag. Fix a published error
with a new release. A failed upload or publication leaves a draft; inspect its run
and assets, then remove **only the unpublished draft** before rerunning the tag's
`standards-release` workflow using **Run workflow** with that tag selected. Keep the
tag unchanged. If publication succeeded but PR labeling failed, change the PR label
from `autorelease: pending` to `autorelease: tagged`; do not rebuild the release.
References: [GitHub immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases),
[enabling immutability](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes),
and [Release Please token behavior](https://github.com/googleapis/release-please-action#other-actions-on-release-please-prs).
+22
View File
@@ -0,0 +1,22 @@
# Scorecard finding dispositions
Reviewed on 2026-10-10 for the v0.1.0 release. These are repository-process
heuristics, not identified vulnerabilities in shipped code or dependencies.
Static review inspected the reported main revision, current rulesets, the security
policy, and EX-0001. It does not establish an independent human review of historical
changes or predict future maintenance.
| Alert | Disposition and evidence | Reassess |
| --- | --- | --- |
| [#4 Code-Review](https://github.com/KofTwentyTwo/standards/security/code-scanning/4) | Accepted single-maintainer risk under [EX-0001](../../exceptions/register.md#ex-0001). Outside contributors cannot merge; PR checks, AI review, CodeQL, signed commits, and maintainer merge remain required. Scorecard counts human approvals, so it does not represent these compensating controls. | 2027-01-10, or when a second maintainer joins |
| [#3 Maintained](https://github.com/KofTwentyTwo/standards/security/code-scanning/3) | Age heuristic: GitHub reports creation on 2026-10-09, with recent feature PRs and active CI. The message identifies a repository younger than 90 days, rather than abandoned shipped software. | 2027-01-10 |
| [#1 Branch-Protection](https://github.com/KofTwentyTwo/standards/security/code-scanning/1) | Accepted human-approval gap under EX-0001. Active `protect-main` has no bypass and enforces PR-only squash merges, nine required checks, signed commits, linear history, and resolved threads. Human approval by another maintainer is unavailable. | 2027-01-10, or when a second maintainer joins |
Keep the accepted risk visible; dismissal is not a claim that the missing second
maintainer exists. Do not disable Scorecard or weaken protections. Reopen and
reassess these dispositions when their assumptions change. The LOW best-practices
badge and MEDIUM fuzzing recommendations remain open for follow-up.
No OpenVEX statement is fabricated for these process findings: no vulnerable shipped
component or exploit was identified. Any actual vulnerability present but not
exploitable needs the OpenVEX statement required by K22-SEC-42.
+83
View File
@@ -0,0 +1,83 @@
# Threat model: standards distribution
- **Version reviewed:** v0.1.0 release-bundle implementation
- **Last updated:** 2026-10-10
- **Status:** Implementation reviewed; provisioning is verified in the release checklist.
## Product and actors
This public repository distributes engineering guidance, configurations, templates,
and scripts. Consumers download a versioned bundle or call reusable workflows pinned
to a commit. The bundle does not run an application or collect consumer data.
Contributors submit changes; the maintainer reviews and merges PRs, creates protected
version tags, and approves publication. GitHub-hosted Actions runners build artifacts;
GitHub stores releases, environment secrets, and attestations. Upstream Actions are
third-party code trusted only at reviewed, pinned versions.
## Data flow and boundaries
```mermaid
flowchart LR
contributor[Contributor] --> pr[Pull request]
pr --> gates[CI and maintainer review]
gates --> main[Protected main]
main --> proposal[Version and changelog PR]
proposal --> gates
main --> tag[Maintainer-created protected tag]
tag --> build[Reusable gated build]
build --> draft[Draft assets and attestations]
draft --> approval[Release environment approval]
approval --> release[Immutable GitHub Release]
release --> consumer[Consumer verifies and extracts]
```
Untrusted PR content cannot access version-bot or publication secrets. The
`release-automation` environment admits only protected `main`; its App key is used
after CI succeeds. The `release` environment admits only protected `v*` tags and
requires maintainer approval. Tokens are scoped per job and revoked on completion.
Publishing checks GitHub's immutability setting before creating the draft.
## External interfaces and assets
| Interface | Data and authentication | Validation |
| --- | --- | --- |
| Git checkout and archive | Public source, tagged commit | Main ancestry, committed version/manifest/changelog |
| GitHub REST API | App installation token or job-scoped `GITHUB_TOKEN` over HTTPS | Release PR, maintainer-approved preflight checklist, immutability setting |
| Actions secrets | App private key in named environments | Deployment policy, reviewed job definitions, no repository secret |
| OIDC attestations | GitHub-signed build provenance | Consumer `gh attestation verify` |
| Release downloads | Versioned archives, manifest, notes, checksums | HTTPS, checksum and provenance verification |
Assets are the App credential, integrity of standards and executable tooling,
protected source history, release tags, artifact bytes, and distribution availability.
## Threats and controls
| Threat / STRIDE category | Control | Remaining condition |
| --- | --- | --- |
| Forged or malicious contribution / spoofing | Signed commits, DCO, PR gates and maintainer review | Review remains accountable to a human |
| Modified source or artifact / tampering | Commit-based archives, checksums, attestations, immutable publication | Consumer must verify downloads |
| Disputed release origin / repudiation | Protected tagged commit, manifest SHA, release checklist, attestations | Maintainer reviews checklist evidence |
| Credential theft / information disclosure | Environment restrictions, scoped App installation, ephemeral tokens, secret scans | EX-0006 must remain open and unexpired |
| Failed or duplicate publication / denial of service | Timeouts, per-tag concurrency, draft-first upload, no replacement | Failed unpublished drafts need maintainer recovery |
| Workflow privilege escalation | SHA-pinned Actions, no cached release dependencies, no App ruleset bypass | Compromised trusted action remains a supply-chain risk |
## Exceptions and review
[EX-0006](../../exceptions/register.md#ex-0006) covers main-only access to the version
bot's key. Existing single-maintainer review uses
[EX-0001](../../exceptions/register.md#ex-0001). Review this model with the implementation
PR and each MINOR/MAJOR checklist; update it when credentials, channels, or workflow
trust boundaries change.
## Review log
- **2026-10-10, v0.1.0:** Reviewed source-only packaging, release-PR validation,
preflight approval, App token scopes, environment restrictions, provenance, and
immutable publication. Main and tag protections remain in place. Most likely
failure is incomplete draft publication; recovery retains the tag and replaces
no published assets. Highest-impact threat is a compromised credential or upstream
Action; restricted environments, scoped installation, reviewed SHA pins, and
required PR gates limit its reach. Provisioning and live checks are recorded in
the first-release checklist. Existing Scorecard process findings are recorded in
[their dispositions](scorecard-dispositions.md); EX-0001 retains the accepted
single-maintainer risk.
+26
View File
@@ -10,6 +10,7 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
| [EX-0003](#ex-0003) | Open | K22-DEP-02 (license allowlist) | gclo | Reviewed yearly, next on 2027-10-09 |
| [EX-0004](#ex-0004) | Open | K22-CI-30, K22-REL-04 (shared release workflow) | gclo | When the shared reusable release workflow ships, at the latest 2027-04-09 |
| [EX-0005](#ex-0005) | Open | K22-AI-10 (agents via PR only), K22-AI-11 (in part: unconfirmed pushes) | second-brain, nix, Praetor, Jellyfin | Reviewed yearly, next on 2027-10-10 |
| [EX-0006](#ex-0006) | Open | K22-CI-16 (tag-only environment secrets) | standards | 2027-04-10; review 2027-01-10 |
## EX-0001
@@ -143,3 +144,28 @@ Open and closed exceptions to the KofTwentyTwo standards. The process is in
requests per `K22-AI-10`.
- **Expires:** reviewed yearly, next on 2027-10-10.
- **Owner:** the maintainer.
## EX-0006
**Main-only release-version bot credential**
- **Status:** Open; approved with the reviewed release-bundle implementation PR.
- **Requirements:** `K22-CI-16` restricts remaining secrets to environments whose
deployment rules allow only protected `v*` tags.
- **Scope:** `KofTwentyTwo/standards`, only `RELEASE_APP_PRIVATE_KEY` in the
`release-automation` environment for the `ci.yml` release-PR job on protected `main`.
- **Reason:** Release Please needs a GitHub App token to create version PRs that
trigger CI without enabling PR creation by `GITHUB_TOKEN`. No native GitHub App
OIDC exchange is configured; the private key is therefore needed before a tag exists.
- **Risk:** compromised version-PR automation could obtain the App key and exercise
the App's repository permissions.
- **Compensating controls:** App installation limited to this repository; Contents and
Pull requests write, Administration read only; no ruleset bypass; main-only
environment deployment policy; job runs only after all main CI gates succeed;
SHA-pinned actions; short-lived scoped installation tokens revoked after each job;
all resulting PRs still require normal checks and maintainer review. The bot neither
creates release tags nor signs or publishes packages. Publication still uses the
protected tag-only `release` environment and GitHub's immutable-releases setting.
- **Expires:** 2027-04-10, or earlier when version proposals use credential federation.
- **Owner:** the maintainer.
- **Review date:** 2027-01-10.
+20
View File
@@ -0,0 +1,20 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "simple",
"include-component-in-tag": false,
"initial-version": "0.1.0",
"changelog-sections": [
{ "type": "security", "section": "Security", "hidden": false },
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "perf", "section": "Performance", "hidden": false },
{ "type": "refactor", "section": "Refactoring", "hidden": false },
{ "type": "build", "section": "Build", "hidden": false },
{ "type": "ci", "section": "Automation", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false }
],
"packages": {
".": { "version-file": "version.txt" }
}
}
+98
View File
@@ -0,0 +1,98 @@
#Requires -Version 7.4
<#
.SYNOPSIS
Build versioned standards archives from a Git commit.
.DESCRIPTION
Uses git archive so untracked files and local edits cannot enter a release. Writes
ZIP, tar.gz, source manifest, release notes, and SHA256SUMS without overwriting output.
.PARAMETER Version
Stable semantic version matching the committed version.txt and release manifest.
.PARAMETER RepositoryPath
Local source repository; defaults to the parent directory of this script.
.PARAMETER Ref
Commit to package; defaults to HEAD.
.PARAMETER OutputPath
New output directory; defaults to artifacts/standards in the source repository.
.PARAMETER ChecklistUrl
Completed release checklist to link in the notes, when applicable.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidatePattern('^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$')]
[string] $Version,
[string] $RepositoryPath = (Split-Path -Parent $PSScriptRoot),
[ValidateNotNullOrEmpty()]
[string] $Ref = 'HEAD',
[string] $OutputPath,
[AllowEmptyString()]
[string] $ChecklistUrl = ''
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
$repository = (Resolve-Path -LiteralPath $RepositoryPath).Path
$commit = (git -C $repository rev-parse --verify "$Ref^{commit}").Trim()
$committedVersion = (git -C $repository show "${commit}:version.txt").Trim()
$releaseManifest = (git -C $repository show "${commit}:.release-please-manifest.json") | ConvertFrom-Json -AsHashtable
if($committedVersion -ne $Version -or $releaseManifest['.'] -ne $Version)
{
throw "Version $Version does not match version.txt and the release manifest at $commit."
}
$changelog = (git -C $repository show "${commit}:CHANGELOG.md") -join "`n"
$escapedVersion = [regex]::Escape($Version)
$section = [regex]::Match($changelog, "(?ms)^## (?:\[$escapedVersion\](?:\([^\r\n]*\))?|$escapedVersion)(?=\s|$)[^\r\n]*\r?\n.*?(?=^## |\z)")
if(-not $section.Success)
{
throw "CHANGELOG.md has no release notes for $Version at $commit."
}
$notes = $section.Value.Trim()
if($ChecklistUrl)
{
if($ChecklistUrl -notmatch '^https://github\.com/[^/]+/[^/]+/issues/[1-9][0-9]*$')
{
throw 'ChecklistUrl must be a GitHub issue URL.'
}
$notes += "`n`nRelease checklist: $ChecklistUrl"
}
if(-not $OutputPath)
{
$OutputPath = Join-Path $repository 'artifacts/standards'
}
$outputDirectory = [System.IO.Path]::GetFullPath($OutputPath)
if(Test-Path -LiteralPath $outputDirectory)
{
throw "Output directory already exists: $outputDirectory. Choose a new directory."
}
New-Item -ItemType Directory -Path $outputDirectory | Out-Null
$name = "KofTwentyTwo-standards-$Version"
foreach($format in @('zip', 'tar.gz'))
{
$archive = Join-Path $outputDirectory "$name.$format"
git -C $repository archive "--format=$format" "--prefix=$name/" "--output=$archive" $commit
}
$manifest = [ordered]@{
name = 'KofTwentyTwo/standards'
version = $Version
tag = "v$Version"
commit = $commit
repository = 'https://github.com/KofTwentyTwo/standards'
licenses = @('CC-BY-4.0 (documentation)', 'MIT (tooling)', 'Apache-2.0 (Kingsrook files; see NOTICE)')
}
$utf8 = [System.Text.UTF8Encoding]::new($false)
$manifestText = ($manifest | ConvertTo-Json -Depth 4).Replace("`r`n", "`n") + "`n"
[System.IO.File]::WriteAllText((Join-Path $outputDirectory "$name.manifest.json"), $manifestText, $utf8)
[System.IO.File]::WriteAllText((Join-Path $outputDirectory "$name.notes.md"), $notes.Replace("`r`n", "`n") + "`n", $utf8)
$checksums = foreach($file in (Get-ChildItem -LiteralPath $outputDirectory -File | Sort-Object -Property Name))
{
$hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$hash $($file.Name)"
}
[System.IO.File]::WriteAllText((Join-Path $outputDirectory 'SHA256SUMS'), ($checksums -join "`n") + "`n", $utf8)
Write-Output "Built $name from $commit in $outputDirectory"
+122
View File
@@ -0,0 +1,122 @@
#Requires -Version 7.4
<#
.SYNOPSIS
Exercise standards packaging with an isolated local Git fixture.
.DESCRIPTION
Checks committed content, hidden files, both archive formats, determinism, hashes,
version validation, release notes, and refusal to overwrite existing output.
#>
[CmdletBinding()]
param()
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true
<#
.SYNOPSIS
Fail a behavioral assertion with a descriptive message.
#>
function Test-Condition
{
param([bool] $Condition, [string] $Message)
if(-not $Condition) { throw "FAIL: $Message" }
Write-Output "PASS: $Message"
}
<#
.SYNOPSIS
Verify an operation refuses invalid input for the expected reason.
#>
function Test-Exception
{
param([scriptblock] $Operation, [string] $Pattern)
$failure = $null
try { & $Operation | Out-Null }
catch { $failure = $_.Exception.Message }
Test-Condition ($null -ne $failure -and $failure -match $Pattern) "rejects $Pattern"
}
$builder = Join-Path $PSScriptRoot 'Build-StandardsPackage.ps1'
$fixtureRoot = Join-Path ([System.IO.Path]::GetTempPath()) "standards-package-$([guid]::NewGuid().ToString('N'))"
$source = Join-Path $fixtureRoot 'source'
New-Item -ItemType Directory -Path $source | Out-Null
git init --quiet $source
git -C $source config core.autocrlf false
git -C $source config core.hooksPath /dev/null
Set-Content -LiteralPath (Join-Path $source 'version.txt') -Value '1.2.3'
Set-Content -LiteralPath (Join-Path $source '.release-please-manifest.json') -Value '{".":"1.2.3"}'
Set-Content -LiteralPath (Join-Path $source '.editorconfig') -Value 'root = true'
Set-Content -LiteralPath (Join-Path $source 'README.md') -Value 'committed content'
Set-Content -LiteralPath (Join-Path $source 'CHANGELOG.md') -Value @'
# Changelog
## [1.2.4](https://example.com/next) (2026-10-11)
Future notes.
## [1.2.3](https://example.com/current) (2026-10-10)
### Security
Current notes.
## 1.2.2
Older notes.
'@
git -C $source add -- .
git -C $source -c user.name=Fixture -c user.email=fixture@example.invalid -c commit.gpgsign=false commit --quiet -m 'test: packaging fixture'
Set-Content -LiteralPath (Join-Path $source 'README.md') -Value 'uncommitted content'
Set-Content -LiteralPath (Join-Path $source 'local-only.txt') -Value 'untracked content'
$first = Join-Path $fixtureRoot 'first'
$second = Join-Path $fixtureRoot 'second'
$checklist = 'https://github.com/KofTwentyTwo/standards/issues/123'
& $builder -Version '1.2.3' -RepositoryPath $source -OutputPath $first -ChecklistUrl $checklist
& $builder -Version '1.2.3' -RepositoryPath $source -OutputPath $second -ChecklistUrl $checklist
$prefix = 'KofTwentyTwo-standards-1.2.3'
$zip = [System.IO.Compression.ZipFile]::OpenRead((Join-Path $first "$prefix.zip"))
try
{
$names = @($zip.Entries.FullName)
Test-Condition ($names -contains "$prefix/.editorconfig") 'ZIP preserves hidden configuration files'
Test-Condition (-not ($names -match '/(?:\.git/|local-only\.txt$)')) 'ZIP excludes Git metadata and untracked files'
$reader = [System.IO.StreamReader]::new($zip.GetEntry("$prefix/README.md").Open())
try { Test-Condition ($reader.ReadToEnd().Trim() -eq 'committed content') 'ZIP contains committed content despite local edits' }
finally { $reader.Dispose() }
}
finally { $zip.Dispose() }
$extracted = Join-Path $fixtureRoot 'extracted'
New-Item -ItemType Directory -Path $extracted | Out-Null
tar -xzf (Join-Path $first "$prefix.tar.gz") -C $extracted
Test-Condition ((Get-Content -LiteralPath (Join-Path $extracted "$prefix/README.md") -Raw).Trim() -eq 'committed content') 'tar.gz contains committed content'
Test-Condition (Test-Path -LiteralPath (Join-Path $extracted "$prefix/.editorconfig")) 'tar.gz preserves hidden configuration files'
$manifest = Get-Content -LiteralPath (Join-Path $first "$prefix.manifest.json") -Raw | ConvertFrom-Json
Test-Condition ($manifest.version -eq '1.2.3' -and $manifest.commit -eq (git -C $source rev-parse HEAD)) 'manifest identifies version and exact source commit'
$notes = Get-Content -LiteralPath (Join-Path $first "$prefix.notes.md") -Raw
Test-Condition ($notes -match 'Current notes' -and $notes -notmatch 'Older notes|Future notes' -and $notes.Contains($checklist)) 'notes contain only this release and its checklist'
foreach($file in (Get-ChildItem -LiteralPath $first -File))
{
Test-Condition ((Get-FileHash -LiteralPath $file.FullName).Hash -eq (Get-FileHash -LiteralPath (Join-Path $second $file.Name)).Hash) "$($file.Name) is reproducible"
}
$checksums = @(Get-Content -LiteralPath (Join-Path $first 'SHA256SUMS'))
Test-Condition ($checksums.Count -eq 4) 'checksums cover every asset except the checksum file itself'
foreach($line in $checksums)
{
$hash, $fileName = $line -split ' ', 2
Test-Condition ($hash -eq (Get-FileHash -LiteralPath (Join-Path $first $fileName)).Hash.ToLowerInvariant()) "$fileName matches its checksum"
}
Test-Exception { & $builder -Version '1.2.4' -RepositoryPath $source -OutputPath (Join-Path $fixtureRoot 'mismatch') } 'does not match'
Test-Exception { & $builder -Version '01.2.3' -RepositoryPath $source } 'ValidatePattern|validation|pattern'
Test-Exception { & $builder -Version '1.2.3' -RepositoryPath $source -OutputPath $first } 'already exists'
Test-Exception { & $builder -Version '1.2.3' -RepositoryPath $source -OutputPath (Join-Path $fixtureRoot 'invalid-url') -ChecklistUrl 'http://example.com/123' } 'GitHub issue URL'
Set-Content -LiteralPath (Join-Path $source 'CHANGELOG.md') -Value '# Changelog without release notes'
git -C $source add -- CHANGELOG.md
git -C $source -c user.name=Fixture -c user.email=fixture@example.invalid -c commit.gpgsign=false commit --quiet -m 'test: missing release notes fixture'
Test-Exception { & $builder -Version '1.2.3' -RepositoryPath $source -OutputPath (Join-Path $fixtureRoot 'missing-notes') } 'no release notes'
Write-Output "All standards package tests passed. Fixture: $fixtureRoot"
+1
View File
@@ -0,0 +1 @@
0.0.0
+2
View File
@@ -7,3 +7,5 @@ rules:
# not yet understood by actionlint 1.7.12, which is also a required gate.
ignore:
- ci.yml
- release.yml
- release-standards.yml