chore: repin source lock to merged Fallow commit #554
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| tags: ["v*.*.*"] | |
| pull_request: | |
| branches: [main] | |
| permissions: {} | |
| jobs: | |
| validate: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| # PyYAML reaches the runner image transitively rather than as a declared | |
| # package, so treat it as a job dependency instead of a side effect of | |
| # whichever step happens to need it first. Installing into the user site | |
| # leaves the system distribution in place: it ships no RECORD file, so | |
| # pip cannot uninstall it and an upgrade in place would fail the job. | |
| - name: Install Python dependencies | |
| run: | | |
| python3 -c 'import yaml' 2>/dev/null || | |
| python3 -m pip install --quiet --disable-pip-version-check \ | |
| --user --break-system-packages pyyaml | |
| python3 -c 'import yaml; print("PyYAML", yaml.__version__, "from", yaml.__file__)' | |
| - name: Validate JSON files | |
| run: | | |
| echo "Validating JSON files..." | |
| while IFS= read -r -d '' f; do | |
| echo " Checking $f" | |
| python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$f" | |
| done < <(find . -name "*.json" -not -path "./node_modules/*" -print0) | |
| echo "All JSON files are valid." | |
| - name: Validate SKILL.md frontmatter | |
| run: python3 scripts/validate_skill_frontmatter.py . | |
| - name: Validate skills.sh discovery | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/skills-home" "$RUNNER_TEMP/skills-config" "$RUNNER_TEMP/skills-cache" | |
| HOME="$RUNNER_TEMP/skills-home" \ | |
| XDG_CONFIG_HOME="$RUNNER_TEMP/skills-config" \ | |
| XDG_CACHE_HOME="$RUNNER_TEMP/skills-cache" \ | |
| npx -y skills@1.5.3 add . --list | |
| - name: Validate Claude plugin | |
| run: npx -y @anthropic-ai/claude-code@2.1.218 plugin validate . --strict | |
| - name: Validate Codex plugin | |
| run: | | |
| python3 - <<'PY' | |
| import json | |
| import struct | |
| from pathlib import Path | |
| root = Path("fallow") | |
| manifest = json.loads((root / ".codex-plugin/plugin.json").read_text()) | |
| interface = manifest["interface"] | |
| assert manifest["name"] == "fallow" | |
| assert manifest["skills"] == "./skills/" | |
| assert len(manifest["name"]) <= 64 | |
| assert len(manifest["description"]) <= 1024 | |
| assert len(manifest["author"]["name"]) <= 120 | |
| assert len(interface["displayName"]) <= 30 | |
| assert "\n" not in interface["shortDescription"] | |
| assert len(interface["shortDescription"]) <= 30 | |
| assert len(interface["longDescription"]) <= 4000 | |
| assert len(interface["developerName"]) <= 80 | |
| assert interface["category"] == "Developer Tools" | |
| assert len(interface["capabilities"]) <= 20 | |
| assert 1 <= len(interface["defaultPrompt"]) <= 3 | |
| assert all(len(prompt) <= 128 for prompt in interface["defaultPrompt"]) | |
| for field in ("composerIcon", "logo"): | |
| asset = root / interface[field] | |
| assert asset.is_file(), f"Missing {field}: {asset}" | |
| assert asset.stat().st_size <= 5 * 1024 * 1024 | |
| with asset.open("rb") as handle: | |
| assert handle.read(8) == b"\x89PNG\r\n\x1a\n" | |
| handle.read(8) | |
| width, height = struct.unpack(">II", handle.read(8)) | |
| assert width == height | |
| assert 48 <= width <= 4096 | |
| marketplace = json.loads(Path(".agents/plugins/marketplace.json").read_text()) | |
| entry = marketplace["plugins"][0] | |
| assert entry["name"] == manifest["name"] | |
| assert entry["source"] == {"source": "local", "path": "./fallow"} | |
| assert entry["policy"]["installation"] == "AVAILABLE" | |
| unsupported = [ | |
| path for path in root.rglob("*") | |
| if path.name in {".mcp.json", ".app.json"} | |
| ] | |
| assert not unsupported, f"Unsupported skills-only files: {unsupported}" | |
| for skill in ("fallow", "fallow-review"): | |
| skill_root = root / "skills" / skill | |
| frontmatter = (skill_root / "SKILL.md").read_text().split("---", 2)[1] | |
| assert "\nmetadata:" not in frontmatter | |
| openai_config = skill_root / "agents/openai.yaml" | |
| assert openai_config.is_file(), f"Missing Codex skill interface: {openai_config}" | |
| config_text = openai_config.read_text() | |
| for field in ("interface:", "display_name:", "short_description:", "default_prompt:"): | |
| assert field in config_text, f"Missing {field} in {openai_config}" | |
| PY | |
| - name: Test plugin release tooling | |
| run: python3 -m unittest discover -s scripts -p 'test_*.py' | |
| - name: Build and inspect OpenAI submission archive | |
| id: package | |
| env: | |
| REF_NAME: ${{ github.ref_name }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| run: | | |
| expected_args=() | |
| if [ "$REF_TYPE" = "tag" ]; then | |
| expected_args=(--expected-version "${REF_NAME#v}") | |
| fi | |
| archive=$(python3 scripts/plugin_release.py package "${expected_args[@]}") | |
| archive_name=$(basename "$archive" .zip) | |
| echo "archive=$archive" >> "$GITHUB_OUTPUT" | |
| echo "archive_name=$archive_name" >> "$GITHUB_OUTPUT" | |
| ARCHIVE="$archive" python3 - <<'PY' | |
| import os | |
| from pathlib import Path | |
| from zipfile import ZipFile | |
| archive = Path(os.environ["ARCHIVE"]) | |
| with ZipFile(archive) as bundle: | |
| names = bundle.namelist() | |
| assert ".codex-plugin/plugin.json" in names | |
| assert any(name.startswith("skills/") and name.endswith("/SKILL.md") for name in names) | |
| assert ".claude-plugin/plugin.json" not in names | |
| assert not any(name.startswith(("bin/", "commands/")) for name in names) | |
| assert not any(name.startswith("/") or ".." in Path(name).parts for name in names) | |
| assert not any(name.endswith((".mcp.json", ".app.json")) for name in names) | |
| assert archive.stat().st_size <= 100_000_000 | |
| PY | |
| - name: Check for hardcoded paths | |
| run: | | |
| echo "Checking for hardcoded paths..." | |
| if grep -rn "/Users/" --include="*.md" --include="*.json" .; then | |
| echo "ERROR: Found hardcoded user paths" | |
| exit 1 | |
| fi | |
| echo "No hardcoded paths found." | |
| - name: Check version sync | |
| run: python3 scripts/plugin_release.py check | |
| - name: Require a version bump for plugin content changes | |
| if: github.ref_type != 'tag' && github.event_name != 'workflow_dispatch' | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then | |
| echo "No comparison commit is available; skipping content version check." | |
| exit 0 | |
| fi | |
| if git diff --quiet "$BASE_SHA" "$GITHUB_SHA" -- \ | |
| fallow .claude-plugin/marketplace.json; then | |
| echo "No public plugin content changed." | |
| exit 0 | |
| fi | |
| base_version=$(git show "$BASE_SHA:fallow/.codex-plugin/plugin.json" | | |
| python3 -c 'import json, sys; print(json.load(sys.stdin)["version"])') | |
| current_version=$(python3 scripts/plugin_release.py newer-than "$base_version") | |
| echo "Plugin version changed from $base_version to $current_version." | |
| - name: Test JavaScript tooling | |
| run: node --test scripts/*.test.mjs | |
| - name: Read pinned Fallow source revision | |
| id: source-lock | |
| run: | | |
| node -e 'process.stdout.write("commit=" + require("./source-lock.json").commit + "\n")' \ | |
| >> "$GITHUB_OUTPUT" | |
| - name: Checkout pinned public Fallow source | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| repository: fallow-rs/fallow | |
| ref: ${{ steps.source-lock.outputs.commit }} | |
| path: .source/fallow | |
| persist-credentials: false | |
| - name: Verify public skill source and privacy boundary | |
| env: | |
| FALLOW_SOURCE_DIR: ${{ github.workspace }}/.source/fallow | |
| run: node scripts/check-source-contract.mjs | |
| - name: Upload OpenAI submission archive | |
| if: github.event_name != 'pull_request' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: ${{ steps.package.outputs.archive_name }} | |
| path: ${{ steps.package.outputs.archive }} | |
| if-no-files-found: error | |
| compression-level: 0 | |
| retention-days: 30 | |
| publish-release: | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Verify tag belongs to main | |
| run: | | |
| if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then | |
| echo "::error::Release tags must point to a commit on main" | |
| exit 1 | |
| fi | |
| - name: Build tagged OpenAI submission archive | |
| id: package | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: | | |
| archive=$(python3 scripts/plugin_release.py package \ | |
| --expected-version "${RELEASE_TAG#v}") | |
| echo "archive=$archive" >> "$GITHUB_OUTPUT" | |
| echo "archive_file=$(basename "$archive")" >> "$GITHUB_OUTPUT" | |
| - name: Publish immutable-by-convention release asset | |
| env: | |
| ARCHIVE: ${{ steps.package.outputs.archive }} | |
| ARCHIVE_FILE: ${{ steps.package.outputs.archive_file }} | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then | |
| release_args=() | |
| if [ "$(python3 scripts/plugin_release.py \ | |
| is-prerelease "${RELEASE_TAG#v}")" = "true" ]; then | |
| release_args+=(--prerelease) | |
| fi | |
| gh release create "$RELEASE_TAG" \ | |
| "$ARCHIVE#OpenAI skills-only submission ZIP" \ | |
| --verify-tag \ | |
| --title "$RELEASE_TAG" \ | |
| --generate-notes \ | |
| "${release_args[@]}" | |
| exit 0 | |
| fi | |
| if gh release view "$RELEASE_TAG" --json assets \ | |
| --jq '.assets[].name' | grep -Fxq "$ARCHIVE_FILE"; then | |
| existing_dir=$(mktemp -d) | |
| gh release download "$RELEASE_TAG" \ | |
| --pattern "$ARCHIVE_FILE" \ | |
| --dir "$existing_dir" | |
| cmp "$ARCHIVE" "$existing_dir/$ARCHIVE_FILE" || { | |
| echo "::error::Existing release asset differs from the tagged source" | |
| exit 1 | |
| } | |
| echo "Release asset already exists and is byte-identical." | |
| else | |
| gh release upload "$RELEASE_TAG" \ | |
| "$ARCHIVE#OpenAI skills-only submission ZIP" | |
| fi |