diff --git a/AGENTS.md b/AGENTS.md index 6a11d067..39451f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Use this file for durable repo-local guidance that Codex should follow before ch - Start with [README.md](./README.md), [CONTRIBUTING.md](./CONTRIBUTING.md), [ROADMAP.md](./ROADMAP.md), [`docs/maintainers/subtree-workflow.md`](./docs/maintainers/subtree-workflow.md), and [`docs/maintainers/release-modes.md`](./docs/maintainers/release-modes.md). - Use [`docs/maintainers/plugin-packaging-strategy.md`](./docs/maintainers/plugin-packaging-strategy.md) when the question is about the root marketplace or the independent-plugin packaging stance. +- Use [`docs/maintainers/spi-add-package-automation-plan.md`](./docs/maintainers/spi-add-package-automation-plan.md) for Swift Package Index readiness, submission, and add-package automation. - When a task is really about one child repo's own behavior, read that child repo's docs before reading broadly across the superproject. ## Working Rules @@ -51,6 +52,9 @@ Use this file for durable repo-local guidance that Codex should follow before ch - Keep the same names for the same concepts across `SKILL.md`, `agents/openai.yaml`, docs, automation prompts, scripts, and marketplace metadata. - If docs and scripts disagree, fix the script or narrow the documented contract so they match. - When shipped behavior, active skill inventory, packaging roots, or validation commands change, update the relevant docs and `ROADMAP.md` in the same pass unless Gale explicitly says not to. +- For Swift Package Index work, treat repo-local readiness and external submission as different states. Use [`scripts/spi_add_package.py`](./scripts/spi_add_package.py) for readiness, official issue-form URL generation, browser opening, and hands-free Computer Use handoff. Do not create `SwiftPackageIndex/PackageList` issues with `gh issue create`, do not apply labels manually, do not fork or clone `SwiftPackageIndex/PackageList`, do not edit `packages.json`, do not open PackageList pull requests, and do not trigger PackageList validation or CLA automation through a manual contribution path. The only supported external add-package path is the documented `Add Package(s)` issue form. +- For hands-free SPI submission, use Codex Computer Use against Zen (`app.zen-browser.zen`) only after `scripts/spi_add_package.py hands-free ` opens the prefilled official issue form and prints its handoff. Computer Use may confirm the form, confirm the package URL, click GitHub's `Submit new issue` button, and verify the created issue has the `Add Package` label. If any check fails, stop and report the failure instead of improvising another GitHub action. +- Say `SPI-ready locally`, `SPI Add Package issue submitted`, or `indexed on SPI` only when that exact state has been verified. A blocked issue, missing label, open PackageList PR, or failed external artifact is not successful SPI setup or successful SPI submission. - Default user-facing plugin install and update guidance to Git-backed marketplace sources with `codex plugin marketplace add /` and `codex plugin marketplace upgrade `. Use explicit refs such as `/@vX.Y.Z` only for pinned reproducible installs, and use manual local marketplace roots only for development, unpublished testing, or fallback cases. - For Python-backed repositories in `socket`, use `uv` as the maintainer baseline and declare repo-local dev dependencies in `pyproject.toml` instead of relying on globally installed tools. - Prefer a root or package-local dev group that explicitly includes the Python maintainer tools the repo expects to run, including `pytest`, `ruff`, and `mypy` when those checks are part of the workflow. diff --git a/ROADMAP.md b/ROADMAP.md index 153c35bf..ed281c96 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -147,6 +147,7 @@ In Progress ## Backlog Candidates - [x] [#35](https://github.com/gaelic-ghost/socket/issues/35) / [#37](https://github.com/gaelic-ghost/socket/issues/37): Harden release and PR scripts around delayed GitHub state. Fold the duplicate timing reports into one implementation pass that reviews CI check registration, PR comment/review reads, remote branch/tag visibility, GitHub release visibility, timeout configuration, logs, and tests across `socket` and the reusable `maintain-project-repo` release assets. +- [x] [#39](https://github.com/gaelic-ghost/socket/issues/39): Add a Swift Package Index add-package gate and one-shot script so agents use only the documented `SwiftPackageIndex/PackageList` Add Package issue form, with Zen plus Codex Computer Use as the hands-free browser path. ## History diff --git a/docs/maintainers/spi-add-package-automation-plan.md b/docs/maintainers/spi-add-package-automation-plan.md new file mode 100644 index 00000000..804835ef --- /dev/null +++ b/docs/maintainers/spi-add-package-automation-plan.md @@ -0,0 +1,133 @@ +# Swift Package Index Add-Package Automation Plan + +## Purpose + +This document records the Socket-level guardrail for adding Swift packages to Swift Package Index. + +The real job is to make the valid SPI path boring and impossible to improvise around. SPI submission uses the documented `SwiftPackageIndex/PackageList` `Add Package(s)` issue form. Socket agents must not replace that form with GitHub CLI issue creation, manual labels, PackageList forks, `packages.json` edits, PackageList pull requests, or any other contribution path. + +## Failure Mode To Prevent + +The SwiftASB SPI attempt failed because an agent treated SPI readiness and SPI submission as if they were the same job, then improvised around the documented form path. + +The concrete bad sequence was: + +1. Create a `SwiftPackageIndex/PackageList` issue with `gh issue create`. +2. Try to attach the `Add Package` label without permission. +3. Observe that SPI automation did not run. +4. Fork `SwiftPackageIndex/PackageList`. +5. Edit `packages.json`. +6. Open a PackageList pull request. +7. Trigger external validation and CLA automation. +8. Describe the package as SPI set up even though it was not indexed. + +None of those steps are acceptable as the default SPI submission path. + +## Source-Of-Truth Process + +The single documented add-package path is: + +1. Confirm the package satisfies SPI requirements. +2. Open the official `Add Package(s)` issue form: + +3. Put one public GitHub package URL per line in the `New Packages` field. +4. Submit the issue through the form. +5. Let SPI's own automation create whatever PackageList follow-up it owns. + +Socket tooling may inspect the public issue form definition before opening it. Socket tooling may not create or mutate any PackageList labels, forks, branches, files, or pull requests. + +## One-Shot Script Contract + +The script lives at: + +```bash +scripts/spi_add_package.py +``` + +The intended one-shot command for a package checkout is: + +```bash +uv run /Users/galew/Workspace/gaelic-ghost/socket/scripts/spi_add_package.py hands-free /path/to/package +``` + +The script performs these repo-local checks before any browser action: + +- `Package.swift` exists at the package root. +- `Package.swift` declares Swift tools version 5.0 or later. +- The package remote resolves to a public GitHub HTTPS `.git` URL. +- At least one semantic-version tag exists locally and is visible on the public GitHub remote. +- `swift package dump-package` succeeds, emits parseable JSON, and includes at least one product. +- `swift build` succeeds unless explicitly skipped. +- `swift test` succeeds unless explicitly skipped. +- The package is not already indexed on SPI when the indexed-state check is available. + +The script then fetches the live `SwiftPackageIndex/PackageList` issue form and refuses to continue unless it still contains: + +- form name `Add Package(s)` +- default title `Add ` +- default label `Add Package` +- required `New Packages` field with id `list` + +If the form shape changes, the script stops. It must not invent a replacement submission path. + +Browser-opening modes require complete readiness and the live form-shape check. Skip flags are allowed only for `readiness` and `url` diagnostic runs. They are rejected for `open` and `hands-free`. + +## Modes + +`readiness` + +Runs local readiness and form-shape checks. It prints the official issue-form URL but does not open a browser. + +`url` + +Same checks as `readiness`; the important output is the prefilled official issue-form URL. + +`open` + +Runs checks, then opens the prefilled official issue form in the configured browser. The default browser target is Zen by bundle id: + +```text +app.zen-browser.zen +``` + +`hands-free` + +Runs checks, opens the prefilled official issue form in Zen, and prints a Codex Computer Use handoff. The handoff is the only permitted browser automation plan: + +1. Use Computer Use against `app.zen-browser.zen`. +2. Confirm the page is the `SwiftPackageIndex/PackageList` `Add Package(s)` issue form. +3. Confirm the `New Packages` field contains the package URL exactly once. +4. Click GitHub's `Submit new issue` button. +5. Verify the created issue has the `Add Package` label and report the issue URL. + +If any of those checks fail, stop and report the failure. Do not recover by creating labels, editing the issue through an API, creating a fork, or opening a PR. + +## Forbidden Actions + +The script and any agent using it must not: + +- run `gh issue create` against `SwiftPackageIndex/PackageList` +- add or edit PackageList labels +- fork `SwiftPackageIndex/PackageList` +- clone `SwiftPackageIndex/PackageList` +- edit `packages.json` +- create or push PackageList branches +- open PackageList pull requests +- trigger PackageList validation or CLA automation through a manual contribution path +- call a blocked PackageList issue or PR a successful SPI submission + +## Success Language + +Use exact state language: + +- `SPI-ready locally`: readiness checks passed, but no external SPI issue has been submitted. +- `SPI Add Package issue submitted`: the official issue form was submitted and the created issue has the `Add Package` label. +- `indexed on SPI`: the package page exists on `swiftpackageindex.com`. + +Do not say `submitted`, `set up`, or `indexed` unless the corresponding external state has actually been verified. + +## Future Skill Integration + +Milestone 39 in `plugins/apple-dev-skills/ROADMAP.md` should treat this script contract as the implementation model for the planned Swift Package Index workflow skill. + +The eventual skill should call or re-export this script instead of restating the process in prose. If the script and skill disagree, the script is the safer operating surface until the skill is corrected. diff --git a/plugins/SpeakSwiftlyServer/docs/maintainers/docc-spi-hosting-plan.md b/plugins/SpeakSwiftlyServer/docs/maintainers/docc-spi-hosting-plan.md index e06ad7fd..13c746a6 100644 --- a/plugins/SpeakSwiftlyServer/docs/maintainers/docc-spi-hosting-plan.md +++ b/plugins/SpeakSwiftlyServer/docs/maintainers/docc-spi-hosting-plan.md @@ -104,5 +104,5 @@ Immediate CI expectations: 2. Land the first `.docc` catalog and symbol-doc pass. 3. Add any CI or local build check needed to keep docs generation honest. 4. Add the short tutorial-style walkthrough if the hosted docs still need a clearer first-use path. -5. Submit the package to Swift Package Index. +5. Submit the package to Swift Package Index only through Socket's `scripts/spi_add_package.py` flow or the equivalent official `SwiftPackageIndex/PackageList` Add Package issue form. 6. Review the resulting hosted docs and compatibility pages, then tighten wording or navigation based on what SPI actually renders. diff --git a/plugins/SpeakSwiftlyServer/docs/releases/v3.1.1-release-and-spi-checklist.md b/plugins/SpeakSwiftlyServer/docs/releases/v3.1.1-release-and-spi-checklist.md index 8d92bfa2..c0c78da4 100644 --- a/plugins/SpeakSwiftlyServer/docs/releases/v3.1.1-release-and-spi-checklist.md +++ b/plugins/SpeakSwiftlyServer/docs/releases/v3.1.1-release-and-spi-checklist.md @@ -103,10 +103,13 @@ Once `v3.1.1` is pushed and the GitHub release exists: 1. Submit the repository through the Swift Package Index add-package flow using the repository URL with the `.git` suffix: `https://github.com/gaelic-ghost/SpeakSwiftlyServer.git` + Prefer Socket's guarded script so the only external action is the official Add Package issue form: + `uv run /Users/galew/Workspace/gaelic-ghost/socket/scripts/spi_add_package.py hands-free /path/to/SpeakSwiftlyServer` 2. Wait for the package page and the first build results to appear. 3. Verify that the hosted documentation build picks up the `SpeakSwiftlyServer` DocC catalog cleanly. 4. Review the rendered package page for README presentation, release notes, compatibility results, and hosted-doc navigation. -5. Once the package page exists, use the maintainership flow on Swift Package Index to claim the package and then add the generated compatibility badges back into `README.md` if they look worth keeping. +5. Once the package page exists, use the maintainership flow on Swift Package Index to claim the package. +6. If adding badges, copy the Swift-version and platform compatibility badge Markdown generated by SPI's maintainer page, place it with the existing README badge preamble, and verify the rendered README afterward. ## Post-Submission Follow-Through diff --git a/plugins/agent-plugin-skills/.codex-plugin/plugin.json b/plugins/agent-plugin-skills/.codex-plugin/plugin.json index 88fbfe4a..dc60ccfc 100644 --- a/plugins/agent-plugin-skills/.codex-plugin/plugin.json +++ b/plugins/agent-plugin-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agent-plugin-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Installable maintainer skills for skills-export repositories.", "author": { "name": "Gale", diff --git a/plugins/agent-plugin-skills/pyproject.toml b/plugins/agent-plugin-skills/pyproject.toml index 254f481b..e6d9740d 100644 --- a/plugins/agent-plugin-skills/pyproject.toml +++ b/plugins/agent-plugin-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agent-plugin-skills-maintenance" -version = "6.4.0" +version = "6.4.1" description = "Maintainer-only Python tooling baseline for agent-plugin-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/agent-plugin-skills/uv.lock b/plugins/agent-plugin-skills/uv.lock index b8208496..57bdfbfa 100644 --- a/plugins/agent-plugin-skills/uv.lock +++ b/plugins/agent-plugin-skills/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "agent-plugin-skills-maintenance" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/apple-dev-skills/.codex-plugin/plugin.json b/plugins/apple-dev-skills/.codex-plugin/plugin.json index 743536f7..bd734fdd 100644 --- a/plugins/apple-dev-skills/.codex-plugin/plugin.json +++ b/plugins/apple-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "apple-dev-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Apple development workflows for Codex and Claude Code, including SwiftUI architecture and DocC authoring guidance.", "author": { "name": "Gale", diff --git a/plugins/apple-dev-skills/ROADMAP.md b/plugins/apple-dev-skills/ROADMAP.md index 67a6c627..b3c194c3 100644 --- a/plugins/apple-dev-skills/ROADMAP.md +++ b/plugins/apple-dev-skills/ROADMAP.md @@ -305,14 +305,17 @@ Planned ### Scope - [ ] Add a dedicated Swift Package Index workflow skill for package distribution, documentation hosting, build readiness, metadata, and submission or listing expectations. -- [ ] Cover the parts of SPI work that matter to maintainers shipping public Swift packages, including documentation hosting, build compatibility, supported platform metadata, README and package-surface expectations, and listing hygiene. +- [ ] Cover the parts of SPI work that matter to maintainers shipping public Swift packages, including documentation hosting, build compatibility, supported platform metadata, README compatibility badges, package-surface expectations, and listing hygiene. ### Tickets - [ ] Define the skill boundary so it owns SPI-specific distribution and hosting guidance without replacing the core Swift package build or testing workflows. - [x] Gather the relevant Swift Package Index documentation for package metadata, documentation hosting, build surfaces, listing or submission expectations, and compatibility signals. - [ ] Ship a workflow surface that can help maintainers prepare a package for SPI, diagnose common SPI-facing build or docs issues, and understand what SPI is deriving from the repository. +- [ ] Integrate the Socket `scripts/spi_add_package.py` one-shot readiness, issue-form, Zen, and Codex Computer Use handoff contract into the workflow. +- [ ] Explicitly forbid non-form PackageList actions, including `gh issue create`, manual label edits, PackageList forks, `packages.json` edits, PackageList branches, PackageList PRs, and CLA-triggering contribution paths. - [ ] Cover the relationship between SPI docs hosting, DocC output, README quality, package metadata, and supported platform declarations. +- [ ] Document the post-indexing badge workflow for copying SPI-generated shields.io Swift-version and platform compatibility badges into README preambles. - [ ] Document common SPI failure modes such as unsupported package structure, incomplete metadata, broken docs generation, or platform mismatch signals. - [ ] Add tests and maintainer docs once the workflow shape is stable. diff --git a/plugins/apple-dev-skills/docs/maintainers/swift-package-index-workflow-research.md b/plugins/apple-dev-skills/docs/maintainers/swift-package-index-workflow-research.md index cad8571a..1bb325a6 100644 --- a/plugins/apple-dev-skills/docs/maintainers/swift-package-index-workflow-research.md +++ b/plugins/apple-dev-skills/docs/maintainers/swift-package-index-workflow-research.md @@ -4,17 +4,21 @@ This note captures the current source-backed guidance for the planned Swift Pack ## Listing A Package -The supported path for getting an ordinary Swift package listed on Swift Package Index is the hosted Add a Package flow, which opens an issue in [`SwiftPackageIndex/PackageList`](https://github.com/SwiftPackageIndex/PackageList/issues/new/choose). +The supported path for getting an ordinary Swift package listed on Swift Package Index is the hosted Add Package issue-form flow, which opens an issue in [`SwiftPackageIndex/PackageList`](https://github.com/SwiftPackageIndex/PackageList/issues/new/choose). -Agents should not tell maintainers to start by hand-editing `packages.json` in a fork. `packages.json` remains the canonical backing list, but the public Add Package flow collects package repository URLs in an issue and the PackageList automation opens the pull request that updates `packages.json`. +Agents must not tell maintainers to start by hand-editing `packages.json` in a fork. `packages.json` remains the canonical backing list, but the public Add Package issue form collects package repository URLs in an issue and the PackageList automation owns any downstream pull request that updates `packages.json`. + +Agents must also not create PackageList issues with `gh issue create`, try to apply the `Add Package` label manually, fork or clone `SwiftPackageIndex/PackageList`, push PackageList branches, open PackageList pull requests, or trigger validation and CLA automation through a manual contribution path. The only supported external add-package action is submitting the official Add Package issue form. Use this practical sequence for a first listing: 1. Confirm the package satisfies the public SPI requirements from [Add a Package](https://swiftpackageindex.com/add-a-package) and the [`PackageList` README](https://github.com/SwiftPackageIndex/PackageList). 2. Use a public GitHub repository URL with the protocol and `.git` suffix, for example `https://github.com/owner/package.git`. -3. Click the Add Package(s) button, choose the Add Package(s) issue template, and submit one URL per line through the `New Packages` field. -4. Wait for the PackageList automation to validate the issue body and open the generated pull request. -5. After the package appears on SPI, use the package page's maintainer flow to claim the package and copy SPI's generated compatibility badges if the repository wants them. +3. Use the Socket add-package script or the documented GitHub UI to open the official Add Package issue form. +4. Submit one URL per line through the `New Packages` field. +5. After submitting, verify the created issue has the `Add Package` label. If it does not, report that the documented form path did not complete and stop. +6. After the package appears on SPI, use the package page's maintainer flow to claim the package and copy SPI's generated compatibility badges if the repository wants them. +7. Review the rendered README after the badge change so the badge row still fits the package's existing README preamble. The live click path is: @@ -29,7 +33,27 @@ https://github.com/owner/package.git https://github.com/owner/another-package.git ``` -The issue workflow then runs `add_package.swift`, validates the updated package list, and creates a pull request that changes `packages.json` when there is real work to do. +The issue form defines the `Add Package` label. The issue workflow runs only when that label is present, then SPI's repository automation owns `add_package.swift`, validation, and any generated pull request. Agents must treat those downstream artifacts as SPI-owned implementation details, not as a fallback action surface. + +## Socket Add-Package Automation + +Socket ships a one-shot guardrail script for agents and maintainers: + +```bash +uv run /Users/galew/Workspace/gaelic-ghost/socket/scripts/spi_add_package.py hands-free /path/to/package +``` + +The script performs repo-local readiness, validates the live PackageList issue-form shape, opens the prefilled official Add Package issue form, and prints a Codex Computer Use handoff. Browser-opening modes require complete readiness and reject skip flags. The default browser target is Zen by bundle id `app.zen-browser.zen`. + +The hands-free path is intentionally narrow: + +1. The script opens the prefilled official issue form. +2. Codex Computer Use confirms the page is the `SwiftPackageIndex/PackageList` `Add Package(s)` issue form. +3. Codex Computer Use confirms the `New Packages` field contains the package URL exactly once. +4. Codex Computer Use clicks GitHub's `Submit new issue` button. +5. Codex verifies the created issue has the `Add Package` label and reports the issue URL. + +If any step fails, stop and report the failure. Do not recover by creating an issue with `gh`, adding labels, forking PackageList, editing `packages.json`, or opening a PackageList PR. ## Requirements To Check Before Submission @@ -38,14 +62,40 @@ Before recommending submission, verify the package against the live SPI requirem - The repository is publicly accessible. - `Package.swift` exists at the repository root. - The package is written in Swift 5.0 or later. -- The package has at least one semantically versioned release tag. -- `swift package dump-package` emits valid JSON with the latest Swift toolchain available to the maintainer. +- The package has at least one semantically versioned release tag visible on the public remote. +- `swift package dump-package` emits valid JSON with the latest Swift toolchain available to the maintainer and reports at least one product. - The package URL includes a protocol, usually `https`, and the `.git` suffix. - The package compiles. - The package content complies with SPI's code of conduct. The current PackageList issue template also states that each package must contain at least one product and at least one product must be usable from other Swift apps. Keep that check in the workflow skill even though the shorter public Add Package page does not currently show it in the same list. +## Compatibility Badges + +SPI recommends adding shields.io compatibility badges to a package README after the package is listed. The practical job of these badges is to show consumers the current Swift-version and platform compatibility that SPI computes from its own build matrix. + +Use the package page's `Do you maintain this package?` maintainer flow as the source of truth for badge Markdown. Do not ask maintainers to hand-roll badge URLs as the default path. The maintainer page currently provides separate badges for: + +- Swift version compatibility, using `type=swift-versions` +- platform compatibility, using `type=platforms` + +When adding badges to a README: + +- prefer both badges when the README already has a badge row or package-status preamble +- link each badge back to the package page on `swiftpackageindex.com` +- place the badges with the existing badge group, before the first major README section +- keep CI, release, documentation, and license badges separate from SPI compatibility badges because they answer different maintainer and consumer questions +- verify the rendered README after editing, especially in repositories with screenshots, callouts, or long first-viewport introductions + +This is the generated Markdown shape currently used by SPI, with the owner and repository filled in by the maintainer page: + +```markdown +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FOWNER%2FREPO%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/OWNER/REPO) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FOWNER%2FREPO%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/OWNER/REPO) +``` + +Use that shape only as an explanation or fallback when the maintainer page is unavailable. For normal package work, copy the generated Markdown directly from SPI so repository, casing, and URL escaping match the live package page. + ## Package Collections Are Separate Do not confuse SPI listing with Swift package collections. @@ -60,9 +110,11 @@ The future SPI workflow skill should own: - listing readiness checks - Add Package issue-flow guidance +- Socket add-package script handoff - `.spi.yml` and DocC hosting readiness handoff - package page review after SPI ingestion -- maintainer claim and badge follow-through +- maintainer claim and SPI-generated badge follow-through +- README badge placement and rendering review - package collection clarification when users ask for curated package lists It should not replace the core Swift package build and testing skills. For compile, test, and `dump-package` verification, hand off to the existing Swift package execution workflows. @@ -74,6 +126,12 @@ It should not replace the core Swift package build and testing skills. For compi - [`SwiftPackageIndex/PackageList` Add Package issue template](https://github.com/SwiftPackageIndex/PackageList/blob/main/.github/ISSUE_TEMPLATE/add_package.yml) - [`SwiftPackageIndex/PackageList` issue automation](https://github.com/SwiftPackageIndex/PackageList/blob/main/.github/workflows/issues.yml) - [`SwiftPackageIndex/PackageList` add-package script](https://github.com/SwiftPackageIndex/PackageList/blob/main/.github/add_package.swift) +- [Socket SPI add-package automation plan](../../../../docs/maintainers/spi-add-package-automation-plan.md) +- [GitHub Docs: creating an issue from a URL query](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-an-issue#creating-an-issue-from-a-url-query) +- [GitHub Docs: syntax for issue forms](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms) +- [`SwiftPackageIndex/SwiftPackageIndex-Server` maintainer badge view](https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/blob/main/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex%2BView.swift) +- [`SwiftPackageIndex/SwiftPackageIndex-Server` maintainer badge model](https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/blob/main/Sources/App/Views/PackageController/MaintainerInfo/MaintainerInfoIndex%2BModel.swift) +- [Swift Forums: Shields.io build badges for your packages](https://forums.swift.org/t/shields-io-build-badges-swift-versions-platforms-for-your-packages/54535) - [Swift Package Index Package Collections](https://swiftpackageindex.com/package-collections) - [Swift.org Package Collections](https://www.swift.org/blog/package-collections/) - [Swift Package Index custom package collections announcement](https://swiftpackageindex.com/blog/introducing-custom-package-collections) diff --git a/plugins/apple-dev-skills/pyproject.toml b/plugins/apple-dev-skills/pyproject.toml index a2bc4582..5beca0f3 100644 --- a/plugins/apple-dev-skills/pyproject.toml +++ b/plugins/apple-dev-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "apple-dev-skills-maintainer" -version = "6.4.0" +version = "6.4.1" description = "Maintainer tooling for the apple-dev-skills repository" requires-python = ">=3.9" dependencies = [] diff --git a/plugins/apple-dev-skills/skills/swift-package-build-run-workflow/SKILL.md b/plugins/apple-dev-skills/skills/swift-package-build-run-workflow/SKILL.md index 60afea2c..81c18568 100644 --- a/plugins/apple-dev-skills/skills/swift-package-build-run-workflow/SKILL.md +++ b/plugins/apple-dev-skills/skills/swift-package-build-run-workflow/SKILL.md @@ -16,6 +16,7 @@ Use this skill as the primary execution workflow for non-testing work in existin - Use this skill for package resources, `Bundle.module`, `.process(...)`, `.copy(...)`, `.embedInCode(...)`, and package-local fixture layout decisions. - Use this skill for Metal library packaging, distribution, and the SwiftPM side of Metal-related package work before Xcode-managed Apple toolchain behavior becomes the real concern. - Use this skill for Debug-versus-Release validation, build artifacts, and tagged-release package expectations. +- Use this skill for Swift Package Index readiness checks in package repositories, but hand external add-package submission to Socket's guarded `scripts/spi_add_package.py` issue-form flow when that script is available. - Do not use this skill for package-testing-first work, test-plan execution, XCTest or Swift Testing diagnosis, or test-specific filtering and retries. - Do not use this skill for brand-new package bootstrap from nothing. - Do not use this skill for repo-guidance alignment in an existing package repo. @@ -52,7 +53,12 @@ Use this skill as the primary execution workflow for non-testing work in existin 5. Use `references/cli-command-matrix.md` for agent-executed SwiftPM commands and terminal-first editor workflows. 6. Use `references/package-resources-testing-and-builds.md` when the request touches package resources, Metal artifacts, `Bundle.module`, or Debug/Release and tagged-release validation. 7. If the repo root is ambiguous because Xcode-managed markers are present at the same root, use `references/xcode-handoff-conditions.md` and hand off cleanly to `xcode-build-run-workflow`. -8. Report which parts were agent-executed, the docs relied on, the repo-shape result, and any required next step or handoff. +8. For Swift Package Index add-package work, distinguish local readiness from external submission: + - local readiness may include `Package.swift`, semantic-version tag, `swift package dump-package`, `swift build`, `swift test`, `.spi.yml`, and DocC checks + - external submission must use the official `SwiftPackageIndex/PackageList` Add Package issue form + - when working from Socket, run `uv run /Users/galew/Workspace/gaelic-ghost/socket/scripts/spi_add_package.py hands-free ` and follow its Codex Computer Use handoff + - never create PackageList issues with `gh issue create`, apply labels manually, fork PackageList, edit `packages.json`, or open PackageList PRs +9. Report which parts were agent-executed, the docs relied on, the repo-shape result, and any required next step or handoff. ## Inputs @@ -91,6 +97,7 @@ Use this skill as the primary execution workflow for non-testing work in existin - Stop with `handoff` when the repo root is mixed and Xcode-managed behavior is the safer default. - Stop with `handoff` when the requested work crosses into Xcode project membership, scheme, preview, simulator, or other Xcode-managed concerns. - Stop with `blocked` when no safe SwiftPM-first command path exists for the requested operation. +- Stop with `blocked` when Swift Package Index submission is requested but the package is only locally ready, the official Add Package issue form is unavailable, the Socket guarded script reports incomplete readiness, or the created issue cannot be verified with the `Add Package` label. ## Fallbacks and Handoffs @@ -133,6 +140,7 @@ Use this skill as the primary execution workflow for non-testing work in existin - Recommend `references/snippets/apple-swift-package-core.md` when the user needs reusable SwiftPM baseline policy wording in an end-user repo. - `references/snippets/apple-swift-package-core.md` +- For Swift Package Index add-package automation in Socket, use `/Users/galew/Workspace/gaelic-ghost/socket/scripts/spi_add_package.py` and `/Users/galew/Workspace/gaelic-ghost/socket/docs/maintainers/spi-add-package-automation-plan.md`. ### Script Inventory diff --git a/plugins/apple-dev-skills/uv.lock b/plugins/apple-dev-skills/uv.lock index 7daead8e..c01b2e09 100644 --- a/plugins/apple-dev-skills/uv.lock +++ b/plugins/apple-dev-skills/uv.lock @@ -8,7 +8,7 @@ resolution-markers = [ [[package]] name = "apple-dev-skills-maintainer" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/cardhop-app/.codex-plugin/plugin.json b/plugins/cardhop-app/.codex-plugin/plugin.json index d76a7f0a..e8012e98 100644 --- a/plugins/cardhop-app/.codex-plugin/plugin.json +++ b/plugins/cardhop-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "cardhop-app", - "version": "6.4.0", + "version": "6.4.1", "description": "Cardhop.app workflow guidance plus a bundled local MCP server for contact capture and updates on macOS.", "author": { "name": "Gale", diff --git a/plugins/cardhop-app/mcp/pyproject.toml b/plugins/cardhop-app/mcp/pyproject.toml index 41d04f9f..fb6c49bf 100644 --- a/plugins/cardhop-app/mcp/pyproject.toml +++ b/plugins/cardhop-app/mcp/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cardhop-app-mcp" -version = "6.4.0" +version = "6.4.1" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/cardhop-app/mcp/uv.lock b/plugins/cardhop-app/mcp/uv.lock index 1bdc384a..85ee6d05 100644 --- a/plugins/cardhop-app/mcp/uv.lock +++ b/plugins/cardhop-app/mcp/uv.lock @@ -93,7 +93,7 @@ wheels = [ [[package]] name = "cardhop-app-mcp" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/dotnet-skills/.codex-plugin/plugin.json b/plugins/dotnet-skills/.codex-plugin/plugin.json index 45adf546..87d4f887 100644 --- a/plugins/dotnet-skills/.codex-plugin/plugin.json +++ b/plugins/dotnet-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dotnet-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Standalone plugin repository for future .NET-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/.codex-plugin/plugin.json b/plugins/productivity-skills/.codex-plugin/plugin.json index 4f59ea6f..f38025a9 100644 --- a/plugins/productivity-skills/.codex-plugin/plugin.json +++ b/plugins/productivity-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "productivity-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Broadly useful productivity workflows for Codex and Claude Code.", "author": { "name": "Gale", diff --git a/plugins/productivity-skills/pyproject.toml b/plugins/productivity-skills/pyproject.toml index 23f0f354..939a84ce 100644 --- a/plugins/productivity-skills/pyproject.toml +++ b/plugins/productivity-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "productivity-skills-maintenance" -version = "6.4.0" +version = "6.4.1" description = "Maintainer-only Python tooling baseline for productivity-skills." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/productivity-skills/uv.lock b/plugins/productivity-skills/uv.lock index 93e527eb..2e8b7eae 100644 --- a/plugins/productivity-skills/uv.lock +++ b/plugins/productivity-skills/uv.lock @@ -40,7 +40,7 @@ wheels = [ [[package]] name = "productivity-skills-maintenance" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/python-skills/.codex-plugin/plugin.json b/plugins/python-skills/.codex-plugin/plugin.json index 36f8220f..2e4eee85 100644 --- a/plugins/python-skills/.codex-plugin/plugin.json +++ b/plugins/python-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "python-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Bundled Python-focused Codex skills for uv bootstrapping, FastAPI and FastMCP scaffolding, FastAPI/FastMCP integration, and pytest workflows.", "author": { "name": "Gale", diff --git a/plugins/python-skills/pyproject.toml b/plugins/python-skills/pyproject.toml index d52d42f6..5d3326e0 100644 --- a/plugins/python-skills/pyproject.toml +++ b/plugins/python-skills/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-skills-maintainer" -version = "6.4.0" +version = "6.4.1" description = "Maintainer tooling for the python-skills repository" requires-python = ">=3.11" dependencies = [] diff --git a/plugins/python-skills/uv.lock b/plugins/python-skills/uv.lock index d33d82dc..527604b7 100644 --- a/plugins/python-skills/uv.lock +++ b/plugins/python-skills/uv.lock @@ -206,7 +206,7 @@ wheels = [ [[package]] name = "python-skills-maintainer" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/rust-skills/.codex-plugin/plugin.json b/plugins/rust-skills/.codex-plugin/plugin.json index a7a912ac..96e6c92c 100644 --- a/plugins/rust-skills/.codex-plugin/plugin.json +++ b/plugins/rust-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rust-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Standalone plugin repository for future Rust-focused Codex skills.", "author": { "name": "Gale", diff --git a/plugins/spotify/.codex-plugin/plugin.json b/plugins/spotify/.codex-plugin/plugin.json index 0b9b0fe6..2f50d847 100644 --- a/plugins/spotify/.codex-plugin/plugin.json +++ b/plugins/spotify/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "spotify", - "version": "6.4.0", + "version": "6.4.1", "description": "Placeholder plugin repository for future Spotify-focused Codex workflows.", "author": { "name": "Gale", diff --git a/plugins/swiftasb-skills/.codex-plugin/plugin.json b/plugins/swiftasb-skills/.codex-plugin/plugin.json index bcaaadc8..dffc8530 100644 --- a/plugins/swiftasb-skills/.codex-plugin/plugin.json +++ b/plugins/swiftasb-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "swiftasb-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Codex skills for explaining SwiftASB and building SwiftUI, AppKit, and Swift package integrations on top of it.", "author": { "name": "Gale", diff --git a/plugins/things-app/.codex-plugin/plugin.json b/plugins/things-app/.codex-plugin/plugin.json index 88c9e113..a81bd495 100644 --- a/plugins/things-app/.codex-plugin/plugin.json +++ b/plugins/things-app/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "things-app", - "version": "6.4.0", + "version": "6.4.1", "description": "Things.app skills and a bundled local MCP server for reminders, planning digests, and structured task workflows.", "author": { "name": "Gale", diff --git a/plugins/things-app/mcp/pyproject.toml b/plugins/things-app/mcp/pyproject.toml index 08334a8d..8f83c134 100644 --- a/plugins/things-app/mcp/pyproject.toml +++ b/plugins/things-app/mcp/pyproject.toml @@ -7,7 +7,7 @@ packages = ["app"] [project] name = "things-mcp" -version = "6.4.0" +version = "6.4.1" requires-python = ">=3.13" dependencies = [ "fastmcp>=3.0.2", diff --git a/plugins/things-app/mcp/uv.lock b/plugins/things-app/mcp/uv.lock index 71ff70a7..982ade08 100644 --- a/plugins/things-app/mcp/uv.lock +++ b/plugins/things-app/mcp/uv.lock @@ -1118,7 +1118,7 @@ wheels = [ [[package]] name = "things-mcp" -version = "6.4.0" +version = "6.4.1" source = { editable = "." } dependencies = [ { name = "fastmcp" }, diff --git a/plugins/things-app/pyproject.toml b/plugins/things-app/pyproject.toml index 70340d4d..befdd5d0 100644 --- a/plugins/things-app/pyproject.toml +++ b/plugins/things-app/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "things-app-maintenance" -version = "6.4.0" +version = "6.4.1" description = "Maintainer-only Python tooling baseline for things-app skills and plugin packaging." requires-python = ">=3.11" dependencies = [] diff --git a/plugins/things-app/uv.lock b/plugins/things-app/uv.lock index 01de69b2..66b565f3 100644 --- a/plugins/things-app/uv.lock +++ b/plugins/things-app/uv.lock @@ -120,7 +120,7 @@ wheels = [ [[package]] name = "things-app-maintenance" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies] diff --git a/plugins/web-dev-skills/.codex-plugin/plugin.json b/plugins/web-dev-skills/.codex-plugin/plugin.json index 937bdd33..4935c8c7 100644 --- a/plugins/web-dev-skills/.codex-plugin/plugin.json +++ b/plugins/web-dev-skills/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "web-dev-skills", - "version": "6.4.0", + "version": "6.4.1", "description": "Standalone plugin repository for future web-focused Codex skills.", "author": { "name": "Gale", diff --git a/pyproject.toml b/pyproject.toml index 866c6b77..ae3283ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "socket-maintenance" -version = "6.4.0" +version = "6.4.1" description = "Root uv tooling baseline for the socket superproject." requires-python = ">=3.11" dependencies = [] diff --git a/scripts/spi_add_package.py b/scripts/spi_add_package.py new file mode 100755 index 00000000..0f13e5bd --- /dev/null +++ b/scripts/spi_add_package.py @@ -0,0 +1,470 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.11" +# /// +"""Prepare and open the single documented Swift Package Index add-package flow.""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +import urllib.error +import urllib.parse +import urllib.request +from dataclasses import dataclass +from pathlib import Path + + +PACKAGE_LIST_ADD_FORM_URL = ( + "https://raw.githubusercontent.com/SwiftPackageIndex/PackageList/" + "main/.github/ISSUE_TEMPLATE/add_package.yml" +) +PACKAGE_LIST_ISSUE_FORM_URL = "https://github.com/SwiftPackageIndex/PackageList/issues/new" +SPI_PACKAGE_BASE_URL = "https://swiftpackageindex.com" +ZEN_BROWSER_BUNDLE_ID = "app.zen-browser.zen" +ZEN_BROWSER_APP_NAME = "Zen" +SEMVER_TAG_RE = re.compile(r"^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$") +SWIFT_TOOLS_VERSION_RE = re.compile(r"//\s*swift-tools-version\s*:\s*(\d+)(?:\.(\d+))?") + + +class SPIAddPackageError(RuntimeError): + """Raised when the package cannot safely proceed to the SPI issue form.""" + + +@dataclass(frozen=True) +class PackageIdentity: + owner: str + repository: str + git_url: str + + @property + def spi_url(self) -> str: + return f"{SPI_PACKAGE_BASE_URL}/{self.owner}/{self.repository}" + + +@dataclass(frozen=True) +class ReadinessResult: + package_root: Path + identity: PackageIdentity + semver_tags: tuple[str, ...] + indexed_state: str + checked_steps: tuple[str, ...] + skipped_steps: tuple[str, ...] + + +def run_command( + args: list[str], + *, + cwd: Path, + check: bool = True, +) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + args, + cwd=cwd, + capture_output=True, + text=True, + check=False, + ) + if check and result.returncode != 0: + stderr = result.stderr.strip() + stdout = result.stdout.strip() + details = stderr or stdout or f"exit status {result.returncode}" + raise SPIAddPackageError( + f"Command failed while preparing SPI submission: {' '.join(args)}\n{details}" + ) + return result + + +def normalize_github_url(remote_url: str) -> PackageIdentity: + candidate = remote_url.strip() + if candidate.startswith("git@github.com:"): + candidate = "https://github.com/" + candidate.removeprefix("git@github.com:") + if candidate.startswith("ssh://git@github.com/"): + candidate = "https://github.com/" + candidate.removeprefix("ssh://git@github.com/") + if candidate.startswith("http://github.com/"): + candidate = "https://github.com/" + candidate.removeprefix("http://github.com/") + if candidate.startswith("https://www.github.com/"): + candidate = "https://github.com/" + candidate.removeprefix("https://www.github.com/") + + parsed = urllib.parse.urlparse(candidate) + if parsed.scheme != "https" or parsed.netloc != "github.com": + raise SPIAddPackageError( + "SPI package URLs must use a public GitHub HTTPS repository URL. " + f"Found remote URL: {remote_url}" + ) + + path_parts = [part for part in parsed.path.strip("/").split("/") if part] + if len(path_parts) != 2: + raise SPIAddPackageError( + "Expected a GitHub repository URL shaped as " + f"`https://github.com/owner/repository.git`, but found: {remote_url}" + ) + + owner, repository = path_parts + repository = repository.removesuffix(".git") + git_url = f"https://github.com/{owner}/{repository}.git" + return PackageIdentity(owner=owner, repository=repository, git_url=git_url) + + +def identity_from_repo(package_root: Path, override_url: str | None) -> PackageIdentity: + if override_url: + return normalize_github_url(override_url) + + result = run_command(["git", "remote", "get-url", "origin"], cwd=package_root) + return normalize_github_url(result.stdout) + + +def discover_semver_tags(package_root: Path) -> tuple[str, ...]: + result = run_command(["git", "tag", "--list"], cwd=package_root) + tags = tuple(sorted(tag for tag in result.stdout.splitlines() if SEMVER_TAG_RE.match(tag))) + if not tags: + raise SPIAddPackageError( + "SPI requires at least one semantic-version release tag before submission. " + "Create and push a real release tag before opening the Add Package form." + ) + return tags + + +def confirm_remote_semver_tag(identity: PackageIdentity, package_root: Path, local_tags: tuple[str, ...]) -> None: + result = run_command(["git", "ls-remote", "--tags", identity.git_url], cwd=package_root) + remote_tags = { + line.rsplit("/", maxsplit=1)[-1].removesuffix("^{}") + for line in result.stdout.splitlines() + if "refs/tags/" in line + } + matching_tags = sorted(tag for tag in local_tags if tag in remote_tags) + if not matching_tags: + raise SPIAddPackageError( + "SPI requires an accessible semantic-version release tag. " + f"Local SemVer tags exist, but none were visible on {identity.git_url}. " + "Push the release tag before opening the Add Package form." + ) + + +def confirm_public_repository(identity: PackageIdentity, package_root: Path) -> None: + run_command(["git", "ls-remote", "--exit-code", identity.git_url, "HEAD"], cwd=package_root) + + +def confirm_swift_tools_version(package_root: Path) -> None: + manifest_prefix = (package_root / "Package.swift").read_text(encoding="utf-8", errors="replace")[:300] + match = SWIFT_TOOLS_VERSION_RE.search(manifest_prefix) + if not match: + raise SPIAddPackageError( + "Package.swift must declare a Swift tools version before SPI submission, " + "for example `// swift-tools-version: 5.10`." + ) + major = int(match.group(1)) + minor = int(match.group(2) or "0") + if (major, minor) < (5, 0): + raise SPIAddPackageError( + "SPI requires packages to be written in Swift 5.0 or later. " + f"Package.swift declares swift-tools-version {major}.{minor}." + ) + + +def dump_package_json(package_root: Path) -> dict[str, object]: + result = run_command(["swift", "package", "dump-package"], cwd=package_root) + try: + package_data = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise SPIAddPackageError( + "SPI requires `swift package dump-package` to emit valid JSON. " + f"JSON parsing failed at line {exc.lineno}, column {exc.colno}: {exc.msg}" + ) from exc + if not isinstance(package_data, dict): + raise SPIAddPackageError("`swift package dump-package` did not emit a JSON object.") + products = package_data.get("products") + if not isinstance(products, list) or not products: + raise SPIAddPackageError( + "SPI requires the package to contain at least one library or executable product." + ) + return package_data + + +def check_spi_index_state(identity: PackageIdentity, timeout: float = 10.0) -> str: + request = urllib.request.Request(identity.spi_url, headers={"User-Agent": "socket-spi-add-package/1"}) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + if response.status == 200: + return "indexed" + return f"unknown-http-{response.status}" + except urllib.error.HTTPError as exc: + if exc.code == 404: + return "not-indexed" + return f"unknown-http-{exc.code}" + except urllib.error.URLError: + return "unknown-network" + + +def fetch_url(url: str, timeout: float = 15.0) -> str: + request = urllib.request.Request(url, headers={"User-Agent": "socket-spi-add-package/1"}) + with urllib.request.urlopen(request, timeout=timeout) as response: + return response.read().decode("utf-8") + + +def validate_live_add_package_form(form_text: str) -> None: + required = { + "form name": "name: Add Package(s)", + "form title": "title: 'Add '", + "default Add Package label": "labels: ['Add Package']", + "New Packages field id": "id: list", + "New Packages label": "label: New Packages", + "required field": "required: true", + } + missing = [label for label, needle in required.items() if needle not in form_text] + if missing: + raise SPIAddPackageError( + "SwiftPackageIndex/PackageList changed its Add Package issue form. " + "Refusing to open a submission until the script is updated. Missing: " + + ", ".join(missing) + ) + + +def build_issue_form_url(identity: PackageIdentity) -> str: + query = urllib.parse.urlencode( + { + "template": "add_package.yml", + "title": f"Add {identity.repository}", + "list": identity.git_url, + } + ) + return f"{PACKAGE_LIST_ISSUE_FORM_URL}?{query}" + + +def run_readiness( + package_root: Path, + *, + override_url: str | None, + skip_build: bool, + skip_tests: bool, + skip_remote_check: bool, + skip_index_check: bool, +) -> ReadinessResult: + package_root = package_root.resolve() + if not package_root.is_dir(): + raise SPIAddPackageError(f"Package root does not exist: {package_root}") + if not (package_root / "Package.swift").is_file(): + raise SPIAddPackageError(f"SPI requires Package.swift at the package root: {package_root}") + + checked_steps: list[str] = ["Package.swift"] + skipped_steps: list[str] = [] + identity = identity_from_repo(package_root, override_url) + + if skip_remote_check: + skipped_steps.append("public repository check") + else: + confirm_public_repository(identity, package_root) + checked_steps.append("public repository") + + semver_tags = discover_semver_tags(package_root) + checked_steps.append("semantic version tags") + confirm_swift_tools_version(package_root) + checked_steps.append("Swift tools version") + + dump_package_json(package_root) + checked_steps.append("swift package dump-package JSON and products") + + if skip_build: + skipped_steps.append("swift build") + else: + run_command(["swift", "build"], cwd=package_root) + checked_steps.append("swift build") + + if skip_tests: + skipped_steps.append("swift test") + else: + run_command(["swift", "test"], cwd=package_root) + checked_steps.append("swift test") + + if (package_root / ".spi.yml").is_file(): + checked_steps.append(".spi.yml present") + else: + skipped_steps.append(".spi.yml not present") + + indexed_state = "unknown-skipped" + if skip_index_check: + skipped_steps.append("SPI indexed-state check") + else: + indexed_state = check_spi_index_state(identity) + checked_steps.append(f"SPI indexed-state: {indexed_state}") + + if skip_remote_check: + skipped_steps.append("remote semantic-version tag visibility") + else: + confirm_remote_semver_tag(identity, package_root, semver_tags) + checked_steps.append("remote semantic-version tag") + + if indexed_state == "indexed": + raise SPIAddPackageError( + f"{identity.owner}/{identity.repository} already appears to be indexed on SPI: " + f"{identity.spi_url}" + ) + + return ReadinessResult( + package_root=package_root, + identity=identity, + semver_tags=semver_tags, + indexed_state=indexed_state, + checked_steps=tuple(checked_steps), + skipped_steps=tuple(skipped_steps), + ) + + +def open_in_browser(url: str, *, browser: str) -> None: + run_command(["open", "-b", browser, url], cwd=Path.cwd()) + + +def computer_use_handoff(url: str, *, result: ReadinessResult, browser: str) -> dict[str, object]: + return { + "mode": "computer-use-hands-free", + "browser_bundle_id": browser, + "preferred_browser_name": ZEN_BROWSER_APP_NAME, + "official_issue_form_url": url, + "allowed_actions": [ + "Use Computer Use get_app_state for the browser.", + "Confirm the page is the SwiftPackageIndex/PackageList Add Package(s) issue form.", + "Confirm the New Packages field contains the package URL exactly once.", + "Click GitHub's Submit new issue button.", + "After creation, verify the issue has the Add Package label and report the URL.", + ], + "forbidden_actions": [ + "Do not run gh issue create.", + "Do not add or edit labels directly.", + "Do not fork SwiftPackageIndex/PackageList.", + "Do not clone SwiftPackageIndex/PackageList.", + "Do not edit packages.json.", + "Do not create or push PackageList branches.", + "Do not open a PackageList pull request.", + "Do not touch CLA-triggering contribution paths.", + ], + "package": { + "owner": result.identity.owner, + "repository": result.identity.repository, + "git_url": result.identity.git_url, + "spi_url": result.identity.spi_url, + }, + } + + +def print_summary(result: ReadinessResult, issue_form_url: str) -> None: + print("SPI readiness passed.") + print(f"Package: {result.identity.owner}/{result.identity.repository}") + print(f"Repository URL: {result.identity.git_url}") + print(f"SPI page: {result.identity.spi_url}") + print(f"SemVer tags found: {', '.join(result.semver_tags[-5:])}") + print("Checked:") + for step in result.checked_steps: + print(f" - {step}") + if result.skipped_steps: + print("Skipped:") + for step in result.skipped_steps: + print(f" - {step}") + print("Official Add Package issue-form URL:") + print(issue_form_url) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Validate Swift Package Index readiness and open only the official " + "SwiftPackageIndex/PackageList Add Package issue form." + ) + ) + parser.add_argument( + "mode", + choices=("readiness", "url", "open", "hands-free"), + help=( + "`readiness` checks only, `url` prints the official issue-form URL, " + "`open` opens the prefilled form, and `hands-free` opens the form plus " + "prints the Codex Computer Use handoff." + ), + ) + parser.add_argument("package_root", nargs="?", default=".", help="Swift package repository root.") + parser.add_argument("--repo-url", help="Override the GitHub package URL.") + parser.add_argument("--browser", default=ZEN_BROWSER_BUNDLE_ID, help="Browser bundle id for open/hands-free.") + parser.add_argument("--skip-build", action="store_true", help="Diagnostic-only for readiness/url: skip `swift build`.") + parser.add_argument("--skip-tests", action="store_true", help="Diagnostic-only for readiness/url: skip `swift test`.") + parser.add_argument( + "--skip-remote-check", + action="store_true", + help="Diagnostic-only for readiness/url: skip public GitHub remote and tag checks.", + ) + parser.add_argument( + "--skip-index-check", + action="store_true", + help="Diagnostic-only for readiness/url: skip SPI already-indexed check.", + ) + parser.add_argument( + "--skip-live-form-check", + action="store_true", + help="Diagnostic-only for readiness/url: skip live PackageList form-shape check.", + ) + return parser.parse_args(argv) + + +def validate_mode_and_skip_flags(args: argparse.Namespace) -> None: + skipped = [ + flag + for flag in ( + "skip_build", + "skip_tests", + "skip_remote_check", + "skip_index_check", + "skip_live_form_check", + ) + if getattr(args, flag) + ] + if args.mode in {"open", "hands-free"} and skipped: + flags = ", ".join("--" + flag.replace("_", "-") for flag in skipped) + raise SPIAddPackageError( + "Browser-opening SPI submission modes require complete readiness and live form checks. " + f"Remove these skip flags before using `{args.mode}`: {flags}" + ) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv or sys.argv[1:]) + try: + validate_mode_and_skip_flags(args) + result = run_readiness( + Path(args.package_root), + override_url=args.repo_url, + skip_build=args.skip_build, + skip_tests=args.skip_tests, + skip_remote_check=args.skip_remote_check, + skip_index_check=args.skip_index_check, + ) + if args.skip_live_form_check: + form_text = "" + else: + form_text = fetch_url(PACKAGE_LIST_ADD_FORM_URL) + validate_live_add_package_form(form_text) + issue_form_url = build_issue_form_url(result.identity) + + print_summary(result, issue_form_url) + + if args.mode in {"open", "hands-free"}: + open_in_browser(issue_form_url, browser=args.browser) + print(f"Opened official Add Package issue form in browser bundle `{args.browser}`.") + + if args.mode == "hands-free": + print("Codex Computer Use handoff:") + print(json.dumps(computer_use_handoff(issue_form_url, result=result, browser=args.browser), indent=2)) + + return 0 + except SPIAddPackageError as exc: + print(f"SPI add-package gate failed: {exc}", file=sys.stderr) + return 1 + except urllib.error.URLError as exc: + print(f"SPI add-package gate failed while reading live SPI/GitHub data: {exc}", file=sys.stderr) + return 1 + except KeyboardInterrupt: + print("SPI add-package gate interrupted.", file=sys.stderr) + return 130 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_spi_add_package.py b/tests/test_spi_add_package.py new file mode 100644 index 00000000..ed4a441c --- /dev/null +++ b/tests/test_spi_add_package.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +MODULE_PATH = Path(__file__).resolve().parent.parent / "scripts" / "spi_add_package.py" +SPEC = importlib.util.spec_from_file_location("spi_add_package", MODULE_PATH) +assert SPEC and SPEC.loader +spi_add_package = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = spi_add_package +SPEC.loader.exec_module(spi_add_package) + + +VALID_FORM = """\ +name: Add Package(s) +description: Add one or more new packages to the Swift Package Index. +title: 'Add ' +labels: ['Add Package'] +body: + - type: textarea + id: list + attributes: + label: New Packages + validations: + required: true +""" + + +def test_normalize_github_url_accepts_ssh_and_adds_git_suffix() -> None: + identity = spi_add_package.normalize_github_url("git@github.com:gaelic-ghost/SwiftASB.git") + + assert identity.owner == "gaelic-ghost" + assert identity.repository == "SwiftASB" + assert identity.git_url == "https://github.com/gaelic-ghost/SwiftASB.git" + + +def test_build_issue_form_url_uses_only_official_template_fields() -> None: + identity = spi_add_package.PackageIdentity( + owner="gaelic-ghost", + repository="SwiftASB", + git_url="https://github.com/gaelic-ghost/SwiftASB.git", + ) + + url = spi_add_package.build_issue_form_url(identity) + + assert url.startswith("https://github.com/SwiftPackageIndex/PackageList/issues/new?") + assert "template=add_package.yml" in url + assert "title=Add+SwiftASB" in url + assert "list=https%3A%2F%2Fgithub.com%2Fgaelic-ghost%2FSwiftASB.git" in url + assert "labels=" not in url + assert "body=" not in url + + +def test_validate_live_add_package_form_rejects_missing_default_label() -> None: + form = VALID_FORM.replace("labels: ['Add Package']\n", "") + + with pytest.raises(spi_add_package.SPIAddPackageError, match="default Add Package label"): + spi_add_package.validate_live_add_package_form(form) + + +def test_validate_live_add_package_form_rejects_missing_list_field() -> None: + form = VALID_FORM.replace("id: list", "id: urls") + + with pytest.raises(spi_add_package.SPIAddPackageError, match="New Packages field id"): + spi_add_package.validate_live_add_package_form(form) + + +def test_validate_mode_rejects_skip_flags_for_hands_free() -> None: + args = spi_add_package.parse_args(["hands-free", ".", "--skip-tests"]) + + with pytest.raises(spi_add_package.SPIAddPackageError, match="complete readiness"): + spi_add_package.validate_mode_and_skip_flags(args) + + +def test_dump_package_json_requires_products(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + completed = spi_add_package.subprocess.CompletedProcess( + args=["swift", "package", "dump-package"], + returncode=0, + stdout='{"name":"Empty","products":[]}', + stderr="", + ) + monkeypatch.setattr(spi_add_package, "run_command", lambda *_args, **_kwargs: completed) + + with pytest.raises(spi_add_package.SPIAddPackageError, match="at least one"): + spi_add_package.dump_package_json(tmp_path) + + +def test_confirm_swift_tools_version_rejects_legacy_manifest(tmp_path: Path) -> None: + (tmp_path / "Package.swift").write_text("// swift-tools-version: 4.2\n", encoding="utf-8") + + with pytest.raises(spi_add_package.SPIAddPackageError, match="Swift 5.0 or later"): + spi_add_package.confirm_swift_tools_version(tmp_path) + + +def test_confirm_remote_semver_tag_requires_pushed_release_tag( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + identity = spi_add_package.PackageIdentity( + owner="gaelic-ghost", + repository="SwiftASB", + git_url="https://github.com/gaelic-ghost/SwiftASB.git", + ) + completed = spi_add_package.subprocess.CompletedProcess( + args=["git", "ls-remote", "--tags", identity.git_url], + returncode=0, + stdout="abc123\trefs/tags/v0.0.1\n", + stderr="", + ) + monkeypatch.setattr(spi_add_package, "run_command", lambda *_args, **_kwargs: completed) + + with pytest.raises(spi_add_package.SPIAddPackageError, match="none were visible"): + spi_add_package.confirm_remote_semver_tag(identity, tmp_path, ("v1.0.0",)) + + +def test_computer_use_handoff_forbids_failed_external_paths() -> None: + result = spi_add_package.ReadinessResult( + package_root=Path("/tmp/SwiftASB"), + identity=spi_add_package.PackageIdentity( + owner="gaelic-ghost", + repository="SwiftASB", + git_url="https://github.com/gaelic-ghost/SwiftASB.git", + ), + semver_tags=("v0.1.0",), + indexed_state="not-indexed", + checked_steps=("Package.swift",), + skipped_steps=(), + ) + + handoff = spi_add_package.computer_use_handoff( + "https://github.com/SwiftPackageIndex/PackageList/issues/new?template=add_package.yml", + result=result, + browser=spi_add_package.ZEN_BROWSER_BUNDLE_ID, + ) + forbidden_text = "\n".join(handoff["forbidden_actions"]) + + assert handoff["browser_bundle_id"] == "app.zen-browser.zen" + assert "gh issue create" in forbidden_text + assert "packages.json" in forbidden_text + assert "fork SwiftPackageIndex/PackageList" in forbidden_text + assert "pull request" in forbidden_text + + +def test_source_does_not_contain_forbidden_package_list_write_commands() -> None: + source = MODULE_PATH.read_text(encoding="utf-8") + + forbidden_snippets = [ + 'run_command(["gh"', + "subprocess.run([\"gh\"", + "create-pull-request", + "--label Add Package", + "SwiftPackageIndex/PackageList.git", + ] + for snippet in forbidden_snippets: + assert snippet not in source diff --git a/uv.lock b/uv.lock index e5e7f4d9..09f2e85a 100644 --- a/uv.lock +++ b/uv.lock @@ -286,7 +286,7 @@ wheels = [ [[package]] name = "socket-maintenance" -version = "6.4.0" +version = "6.4.1" source = { virtual = "." } [package.dev-dependencies]