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
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ jobs:
fi
wait "$test_pid"
- name: Validate package ownership and publication
run: python3 -m unittest discover -s Tests -p 'test_*.py' -v
run: |
python3 -m unittest discover -s Tests -p 'test_*.py' -v
python3 Scripts/check-workflow.py --check-release-workflow
- name: Build relocatable adapter and receipt
run: |
python3 Scripts/package.py --output dist --configuration release
Expand Down
189 changes: 99 additions & 90 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,117 @@ on:
workflow_dispatch:
inputs:
source_run:
description: Successful CI run ID
description: Accepted successful CI run ID (frozen receipt takes precedence on recovery)
type: string
required: true
release_tag:
description: Existing draft release tag bound to the verified commit
required: true
permissions:
actions: read
contents: write
description: Signed annotated version tag (omit for a dry run)
type: string
required: false
publish:
description: Publish the verified release
type: boolean
default: false
permissions: {}
concurrency:
group: release-upload-${{ inputs.release_tag }}
group: plugin-release-${{ github.repository }}-${{ inputs.release_tag || inputs.source_run }}
cancel-in-progress: false
jobs:
upload:
verify:
if: github.repository == 'computer-mcp/plugin-codex' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
SOURCE_RUN: ${{ inputs.source_run }}
RELEASE_TAG: ${{ inputs.release_tag }}
permissions:
contents: read
actions: read
outputs:
artifact_id: ${{ steps.upload.outputs.artifact-id }}
asset_digests: ${{ steps.bundle.outputs.asset_digests }}
helper_digest: ${{ steps.bundle.outputs.helper_digest }}
tag: ${{ steps.bundle.outputs.tag }}
steps:
- name: Verify accepted source and commit-bound draft
id: source
shell: bash
run: |
set -euo pipefail
[[ "$SOURCE_RUN" =~ ^[0-9]+$ ]]
gh api "repos/$GH_REPO/actions/runs/$SOURCE_RUN" > run.json
jq -e --arg repo "$GH_REPO" '
.status == "completed" and .conclusion == "success" and
.event == "push" and .head_branch == "master" and
.path == ".github/workflows/ci.yml" and
.head_repository.full_name == $repo' run.json > /dev/null
source_sha=$(jq -r .head_sha run.json)
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
gh api "repos/$GH_REPO/git/ref/tags/$RELEASE_TAG" > tag-ref.json
[[ $(jq -r .object.type tag-ref.json) == tag ]]
tag_object=$(jq -r .object.sha tag-ref.json)
gh api "repos/$GH_REPO/git/tags/$tag_object" > tag.json
jq -e --arg sha "$source_sha" '.object.type == "commit" and .object.sha == $sha' tag.json > /dev/null
gh api --paginate "repos/$GH_REPO/releases?per_page=100" --slurp > releases.json
jq -e --arg tag "$RELEASE_TAG" --arg sha "$source_sha" '
[ .[][] | select(.tag_name == $tag) ] |
if length == 1 and .[0].draft and .[0].target_commitish == $sha
then .[0] else error("Expected one commit-bound draft release") end
' releases.json > release.json
release_id=$(jq -r .id release.json)
echo "sha=$source_sha" >> "$GITHUB_OUTPUT"
echo "release_id=$release_id" >> "$GITHUB_OUTPUT"
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: '3.13'
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ steps.source.outputs.sha }}
ref: ${{ github.workflow_sha }}
persist-credentials: false
- name: Verify and promote every declared native archive
shell: bash
- name: Verify native CI artifacts and assemble the frozen release bundle
id: bundle
env:
SOURCE_SHA: ${{ steps.source.outputs.sha }}
RELEASE_ID: ${{ steps.source.outputs.release_id }}
GH_TOKEN: ${{ github.token }}
GH_REPOSITORY: ${{ github.repository }}
SOURCE_RUN: ${{ inputs.source_run }}
RELEASE_TAG: ${{ inputs.release_tag }}
RELEASE_PUBLISH: ${{ inputs.publish }}
RELEASE_WORKFLOW_REF: ${{ github.workflow_ref }}
RELEASE_WORKFLOW_SHA: ${{ github.workflow_sha }}
shell: bash
run: |
set -euo pipefail
python3 - <<'PYTHON'
import json, os, re, tomllib
python3 -I Scripts/plugin_release.py verify \
--repository "$GH_REPOSITORY" --source-run "$SOURCE_RUN" \
--tag "$RELEASE_TAG" --publish "$RELEASE_PUBLISH" \
--workflow-ref "$RELEASE_WORKFLOW_REF" --workflow-sha "$RELEASE_WORKFLOW_SHA" \
--output .build/plugin-release/assets
python3 -I - <<'PY'
import hashlib, os
from pathlib import Path
data = Path('Scripts/plugin_release.py').read_bytes()
Path('.build/plugin-release/plugin_release.py').write_bytes(data)
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
output.write('helper_digest=' + hashlib.sha256(data).hexdigest() + '\n')
PY
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
id: upload
with:
name: plugin-release-${{ github.run_id }}-${{ github.run_attempt }}
path: .build/plugin-release/
include-hidden-files: true
if-no-files-found: error
retention-days: 14
publish:
if: inputs.publish
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
outputs:
published: ${{ steps.release.outputs.published }}
release_id: ${{ steps.release.outputs.release_id }}
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
artifact-ids: ${{ needs.verify.outputs.artifact_id }}
path: ${{ runner.temp }}/plugin-release
merge-multiple: true
- name: Verify the helper and publish only the verified bundle
id: release
env:
GH_TOKEN: ${{ github.token }}
GH_REPOSITORY: ${{ github.repository }}
RELEASE_ASSET_DIGESTS: ${{ needs.verify.outputs.asset_digests }}
RELEASE_HELPER_DIGEST: ${{ needs.verify.outputs.helper_digest }}
shell: bash
run: |
python3 -I - <<'PY'
import hashlib, os, subprocess, sys
from pathlib import Path
manifest = tomllib.loads(Path("computer-mcp-plugin.toml").read_text())
if manifest["id"] != "codex" or "v" + manifest["version"] != os.environ["RELEASE_TAG"]:
raise SystemExit("Release tag does not match the accepted plugin manifest")
names = [item["name"] for item in manifest["compatibility"]["artifacts"]]
if not names or len(set(names)) != len(names) or any(not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*\.zip", name) for name in names):
raise SystemExit("Expected unique declared native archives")
Path("archive-names.json").write_text(json.dumps(names))
PYTHON
while IFS= read -r archive; do
name=${archive%.zip}
gh run download "$SOURCE_RUN" --name "$name-$SOURCE_SHA" --dir "artifacts/$name"
python3 Scripts/check-package.py --archive "artifacts/$name/$archive" --receipt "artifacts/$name/receipt.json" > "artifacts/$name/verification.json"
done < <(jq -r '.[]' archive-names.json)
# Upload only after every declared archive passes its exact source-bound check.
upload_asset() {
local file=$1 name=$2 content_type=$3 expected existing
expected=$(sha256sum "$file" | cut -d' ' -f1)
gh api --paginate "repos/$GH_REPO/releases/$RELEASE_ID/assets?per_page=100" --slurp | jq 'add' > assets.json
existing=$(jq -er --arg name "$name" '[.[] | select(.name == $name)] | if length <= 1 then (.[0].id // "") else error("Duplicate release asset") end' assets.json)
if [[ -n "$existing" ]]; then
jq -e --argjson id "$existing" --arg digest "sha256:$expected" '
.[] | select(.id == $id) |
.state == "starter" or (.state == "uploaded" and .digest == $digest)
' assets.json > /dev/null
if jq -e --argjson id "$existing" '.[] | select(.id == $id) | .state == "uploaded"' assets.json > /dev/null; then
return
fi
gh api "repos/$GH_REPO/releases/$RELEASE_ID" | jq -e --arg tag "$RELEASE_TAG" --arg sha "$SOURCE_SHA" '.draft and .tag_name == $tag and .target_commitish == $sha' > /dev/null
gh api --method DELETE "repos/$GH_REPO/releases/assets/$existing"
fi
# Re-read immediately before upload; published releases are immutable.
gh api "repos/$GH_REPO/releases/$RELEASE_ID" | jq -e --arg tag "$RELEASE_TAG" --arg sha "$SOURCE_SHA" '.draft and .tag_name == $tag and .target_commitish == $sha' > /dev/null
gh api --method POST "https://uploads.github.com/repos/$GH_REPO/releases/$RELEASE_ID/assets?name=$name" \
--input "$file" -H "Content-Type: $content_type" > uploaded.json
jq -e --arg digest "sha256:$expected" '.state == "uploaded" and .digest == $digest' uploaded.json > /dev/null
}
while IFS= read -r archive; do
name=${archive%.zip}
upload_asset "artifacts/$name/$archive" "$archive" application/zip
upload_asset "artifacts/$name/receipt.json" "$name.receipt.json" application/json
done < <(jq -r '.[]' archive-names.json)
root = Path(os.environ['RUNNER_TEMP']) / 'plugin-release'
helper = root / 'plugin_release.py'
if helper.is_symlink() or hashlib.sha256(helper.read_bytes()).hexdigest() != os.environ['RELEASE_HELPER_DIGEST']:
raise SystemExit('Release helper differs from the verify job digest')
subprocess.run([sys.executable, '-I', str(helper), 'publish',
'--repository', os.environ['GH_REPOSITORY'],
'--directory', str(root / 'assets'),
'--asset-digests', os.environ['RELEASE_ASSET_DIGESTS']], check=True)
PY
notify-catalog:
needs: publish
if: inputs.publish && needs.publish.outputs.published == 'true'
permissions: {}
uses: ./.github/workflows/notify-catalog.yml
secrets:
CATALOG_APP_PRIVATE_KEY: ${{ secrets.CATALOG_APP_PRIVATE_KEY }}
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ Use this check:
## Task Route

- Before changing versions, dependencies, packaging or release workflows, read
`Documentation/Architecture/VersioningAndRelease.md` and use its existing project check entry points.
`VERSIONING.md` for version rules and `Documentation/Reference/Release.md`
for release facts and project check entry points.

- For repository-native documentation placement, read `Documentation/README.md`
before editing.
Expand Down
8 changes: 5 additions & 3 deletions Documentation/Architecture/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ CONTRIBUTING.md describes contributor checks. Current package facts belong in
Documentation/Architecture/Package.md; commands, formats and operating detail
belong in Documentation/Reference/.

Version and release rules belong in
Documentation/Architecture/VersioningAndRelease.md, and publication steps in
Documentation/Reference/Release.md. Durable transitions or accepted decisions
Version rules belong in the root
[VERSIONING.md](https://github.com/computer-mcp/plugin-codex/blob/master/VERSIONING.md),
which adopts the organization's rules and states repository differences.
Release facts and procedures belong in
[Release](../Reference/Release.md). Durable transitions or accepted decisions
may have separate history records when needed.
GitHub-specific configuration belongs in .github/.

Expand Down
50 changes: 41 additions & 9 deletions Documentation/Architecture/Package.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,44 @@ dependency.
| --- | --- |
| `computer-mcp/swift-sdk` transport fork | Standard northbound MCP transport, tools and results; stdio and complete POSIX frame writes |
| swift-codex | App Server client and Exec client, each with its own lifecycle |
| swift-subprocess 0.4.0 | Child process launch and line I/O |
| swift-argument-parser 1.8.2 | Named options, schema-comparison subcommand, validation and generated CLI help |
| GRDB 7.11.1 | Codex approval, runtime/thread ownership, acceptance run, worktree lease and managed-worktree storage and transactions |

`Scripts/verify-swift-codex-release-gate.sh` verifies that the manifest's exact
SDK version, resolved revision and public Git tag agree. CI requires this check
before packaging. Its dependency notices ship with the
| swift-subprocess | Child process launch and line I/O |
| swift-argument-parser | Named options, schema-comparison subcommand, validation and generated CLI help |
| GRDB | Codex approval, runtime/thread ownership, acceptance run, worktree lease and managed-worktree storage and transactions |

The [release reference](../Reference/Release.md#dependencies-and-verified-combinations)
records dependency requirements, selected revisions and compatibility checks.
swift-crypto and swift-nio are transitive dependencies, owned by their declaring
packages.

The managed line process uses swift-subprocess's explicit input writer,
file-descriptor stdout and stderr sequence. Its child-input owner installs a
no-op `SA_RESTART` SIGPIPE handler once only for the default disposition,
preserving existing handlers and ignored signals. A handler resets on exec,
whereas ignoring SIGPIPE would survive into children. Broken pipe errors from
the library propagate unchanged; a returned short write fails with the adapter's
closed-transport error. The child-input owner follows these ordering and
lifetime rules:

1. Whole frames enter a FIFO queue, with at most one library write outstanding.
A caller-requested EOF follows earlier writes in that same order.
2. Cancellation before admission removes the caller promptly without writing,
including cancellation racing with admission.
3. An admitted write runs in an owner task. Its caller awaits completion even
if cancelled; only process I/O termination can interrupt that write.
4. A positive short count retires input before another caller can be admitted.
Queued and later writes fail closed. A zero count fails that write without
retiring input; thrown library errors propagate unchanged.
5. Shutdown rejects queued writes and proceeds with bounded process-group
termination independently of the active write. Both explicit input finish
and return from the subprocess body wait for that write to settle. Supervisor
exit cancels process I/O even if a descendant retains the pipe. Close allows
up to five seconds for startup, two termination-grace intervals, and one
kill-grace interval before reporting a termination timeout; it never closes
input under an outstanding write.

Shutdown preserves EOF delivery and frame/output limits.

Dependency notices ship with the
adapter artifact, independently of host dependencies.

Argument Parser keeps command structure and help in one declaration. Parsing
Expand Down Expand Up @@ -248,8 +279,9 @@ transfer ownership of earlier work.

Shutdown joins notification/request consumers, performs bounded unsubscription,
marks pending approval records interrupted, and closes the transport. Teardown
closes stdin, waits for graceful exit, sends TERM to the exact owned process
group if necessary, then KILL after another bounded interval and records reaping.
rejects queued input and delivers EOF once any admitted write settles. It waits
for graceful exit, sends TERM to the exact owned process group if necessary,
then KILL after another bounded interval and records reaping.
A private supervisor watches the owner's death and applies the same cleanup.
No process is selected by executable name or a machine-wide search.

Expand Down
4 changes: 2 additions & 2 deletions Documentation/Architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
boundaries. [Documentation](Documentation.md) describes repository documentation
roles and the API documentation scope.

[Versioning and Release](VersioningAndRelease.md) owns component version sources,
compatibility decisions, candidate evidence and release identity.
The [release reference](../Reference/Release.md) records component version
sources, compatibility, candidate evidence and publication mechanics.
Loading
Loading