diff --git a/.github/workflows/publish-action-repo.yml b/.github/workflows/publish-action-repo.yml new file mode 100644 index 0000000..914a9d0 --- /dev/null +++ b/.github/workflows/publish-action-repo.yml @@ -0,0 +1,130 @@ +name: Publish GitHub Action Repository + +on: + release: + types: [published, edited] + workflow_dispatch: + inputs: + release_tag: + description: "Release tag to publish from the source repository" + required: false + default: "" + action_repository: + description: "Dedicated public repository that hosts the Marketplace action" + required: false + default: "hashgraph-online/hol-codex-plugin-scanner-action" + create_repository: + description: "Create the dedicated action repository if it does not exist" + required: false + default: true + type: boolean + +permissions: + contents: read + +concurrency: + group: codex-plugin-scanner-action-publish-${{ github.event.release.tag_name || inputs.release_tag || github.ref }} + cancel-in-progress: false + +jobs: + publish-action-repo: + name: Publish GitHub Action Repository + if: secrets.ACTION_REPO_TOKEN != '' + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.ACTION_REPO_TOKEN }} + ACTION_REPOSITORY: ${{ inputs.action_repository != '' && inputs.action_repository || vars.ACTION_REPOSITORY != '' && vars.ACTION_REPOSITORY || 'hashgraph-online/hol-codex-plugin-scanner-action' }} + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} + CREATE_REPOSITORY: ${{ github.event_name == 'workflow_dispatch' && inputs.create_repository || 'true' }} + SOURCE_REPOSITORY: ${{ github.repository }} + SOURCE_SERVER_URL: ${{ github.server_url }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.event.release.tag_name || inputs.release_tag || github.ref }} + fetch-depth: 0 + + - name: Resolve version + id: version + run: | + TAG="${RELEASE_TAG}" + if [ -z "${TAG}" ]; then + TAG=$(python3 -c "import tomllib; p=tomllib.load(open('pyproject.toml','rb')); print('v' + p['project']['version'])") + fi + VERSION="${TAG#v}" + echo "tag=${TAG}" >> "${GITHUB_OUTPUT}" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + + - name: Ensure action repository exists + run: | + if gh repo view "${ACTION_REPOSITORY}" >/dev/null 2>&1; then + exit 0 + fi + if [ "${CREATE_REPOSITORY}" != "true" ]; then + echo "Action repository ${ACTION_REPOSITORY} does not exist and automatic creation is disabled." >&2 + exit 1 + fi + gh repo create "${ACTION_REPOSITORY}" \ + --public \ + --description "HOL Codex Plugin Scanner GitHub Action" \ + --homepage "${SOURCE_SERVER_URL}/${SOURCE_REPOSITORY}" \ + --disable-wiki + + - name: Sync action repository contents + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.tag }}" + WORKDIR="${RUNNER_TEMP}/action-repository" + git clone "https://x-access-token:${GH_TOKEN}@github.com/${ACTION_REPOSITORY}.git" "${WORKDIR}" + cd "${WORKDIR}" + + if git ls-remote --exit-code origin refs/heads/main >/dev/null 2>&1; then + git fetch origin main + git switch -C main origin/main + else + git switch --orphan main + fi + + find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} + + + cp "${GITHUB_WORKSPACE}/action/action.yml" action.yml + cp "${GITHUB_WORKSPACE}/action/README.md" README.md + cp "${GITHUB_WORKSPACE}/LICENSE" LICENSE + cp "${GITHUB_WORKSPACE}/SECURITY.md" SECURITY.md + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add action.yml README.md LICENSE SECURITY.md + + HAS_HEAD=true + git rev-parse --verify HEAD >/dev/null 2>&1 || HAS_HEAD=false + + if [ "${HAS_HEAD}" = "true" ] && git diff --cached --quiet; then + echo "No action repository content changes detected." + else + git commit -m "chore: publish action bundle ${TAG}" + git push origin HEAD:main + fi + + git tag -f "${TAG}" + git push origin "refs/tags/${TAG}" --force + git tag -f v1 + git push origin refs/tags/v1 --force + + - name: Create or update action repository release + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="${{ steps.version.outputs.tag }}" + NOTES="Published automatically from ${SOURCE_SERVER_URL}/${SOURCE_REPOSITORY}/releases/tag/${TAG}" + + if gh release view "${TAG}" --repo "${ACTION_REPOSITORY}" >/dev/null 2>&1; then + gh release edit "${TAG}" \ + --repo "${ACTION_REPOSITORY}" \ + --title "${TAG}" \ + --notes "${NOTES}" + else + gh release create "${TAG}" \ + --repo "${ACTION_REPOSITORY}" \ + --title "${TAG}" \ + --notes "${NOTES}" + fi diff --git a/README.md b/README.md index d141f06..1357c20 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![CI](https://github.com/hashgraph-online/codex-plugin-scanner/actions/workflows/ci.yml/badge.svg)](https://github.com/hashgraph-online/codex-plugin-scanner/actions/workflows/ci.yml) [![Publish](https://github.com/hashgraph-online/codex-plugin-scanner/actions/workflows/publish.yml/badge.svg)](https://github.com/hashgraph-online/codex-plugin-scanner/actions/workflows/publish.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/hashgraph-online/codex-plugin-scanner/badge)](https://scorecard.dev/viewer/?uri=github.com/hashgraph-online/codex-plugin-scanner) -[![License](https://img.shields.io/github/license/hashgraph-online/codex-plugin-scanner)](./LICENSE) +[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE) [![GitHub Stars](https://img.shields.io/github/stars/hashgraph-online/codex-plugin-scanner?style=social)](https://github.com/hashgraph-online/codex-plugin-scanner/stargazers) [![Lint: ruff](https://img.shields.io/badge/lint-ruff-D7FF64.svg)](https://github.com/astral-sh/ruff) @@ -177,9 +177,30 @@ GitHub Marketplace has two important constraints for actions: - the published action must live in a dedicated public repository with a single root `action.yml` - that repository cannot contain workflow files -Because the scanner repository itself contains CI and release workflows, the Marketplace listing should be published from a separate action-only repository. The scanner release workflow now emits a root-ready bundle zip for that repository on every tagged release. +Because the scanner repository itself contains CI and release workflows, the Marketplace listing should be published from a separate action-only repository. -The source README for that dedicated action repository lives in [action/README.md](action/README.md), and the full publication guide lives in [docs/github-action-marketplace.md](docs/github-action-marketplace.md). +The dedicated action-repository guide now lives directly in [action/README.md](action/README.md). + +### Automated Action Publication + +The source repository can publish the GitHub Action automatically into a dedicated public action repository. + +Configure: + +- repository secret `ACTION_REPO_TOKEN` + It should be a token that can create or update repositories and releases in the target repository. +- optional repository variable `ACTION_REPOSITORY` + Defaults to `hashgraph-online/hol-codex-plugin-scanner-action`. + +When a tagged release is published, [publish-action-repo.yml](./.github/workflows/publish-action-repo.yml) will: + +- create the dedicated action repository if it does not already exist +- sync the root-ready `action.yml`, `README.md`, `LICENSE`, and `SECURITY.md` +- push the immutable release tag such as `v1.2.0` +- move the floating `v1` tag +- create or update the corresponding release in the action repository + +GitHub Marketplace still requires the one-time listing publication step in the dedicated action repository UI, but after that this repository can keep the action repository current automatically. ### Plugin Author Submission Flow diff --git a/action/README.md b/action/README.md index f7327e7..8bcfcd7 100644 --- a/action/README.md +++ b/action/README.md @@ -145,6 +145,8 @@ Use a fine-grained token with `issues:write` on `hashgraph-online/awesome-codex- - Publish immutable releases such as `v1.2.0`. - Move the floating major tag `v1` to the latest compatible release. - Keep this action in its own public repository for GitHub Marketplace publication. +- Configure `ACTION_REPO_TOKEN` in the source repository so `publish-action-repo.yml` can sync this root-ready bundle automatically. +- Optionally set `ACTION_REPOSITORY` in the source repository if the target repository should not be `hashgraph-online/hol-codex-plugin-scanner-action`. ## Source Of Truth diff --git a/tests/test_action_bundle.py b/tests/test_action_bundle.py index 8df3add..e0389c9 100644 --- a/tests/test_action_bundle.py +++ b/tests/test_action_bundle.py @@ -17,7 +17,6 @@ def test_action_metadata_includes_marketplace_branding_and_fallback_install() -> assert 'pip install "$LOCAL_SOURCE"' in action_text assert "submission_enabled:" in action_text assert "submission_issue_urls:" in action_text - assert "actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065" in action_text assert "python3 -m codex_plugin_scanner.action_runner" in action_text @@ -29,12 +28,34 @@ def test_publish_workflow_attaches_marketplace_action_bundle() -> None: assert 'cp action/action.yml "${BUNDLE_ROOT}/action.yml"' in workflow_text -def test_action_bundle_has_root_ready_readme_and_marketplace_guide() -> None: +def test_publish_action_repo_workflow_syncs_action_repository() -> None: + workflow_text = (ROOT / ".github" / "workflows" / "publish-action-repo.yml").read_text(encoding="utf-8") + + assert "Publish GitHub Action Repository" in workflow_text + assert "ACTION_REPO_TOKEN" in workflow_text + assert "hashgraph-online/hol-codex-plugin-scanner-action" in workflow_text + assert 'gh repo create "${ACTION_REPOSITORY}"' in workflow_text + assert 'cp "${GITHUB_WORKSPACE}/action/action.yml" action.yml' in workflow_text + assert 'git push origin refs/tags/v1 --force' in workflow_text + assert 'gh release create "${TAG}"' in workflow_text + + +def test_action_bundle_docs_live_in_action_readme() -> None: action_readme = (ROOT / "action" / "README.md").read_text(encoding="utf-8") - guide = (ROOT / "docs" / "github-action-marketplace.md").read_text(encoding="utf-8") assert "single root `action.yml`" in action_readme - assert "must not contain any workflow files" in guide - assert "dedicated public action repository" in guide + assert "no workflow files" in action_readme + assert "dedicated Marketplace repository" in action_readme + assert "Source Of Truth" in action_readme assert "submission issue" in action_readme - assert "awesome-codex-plugins" in guide + assert "awesome-codex-plugins" in action_readme + assert "publish-action-repo.yml" in action_readme + + +def test_readme_uses_stable_apache_license_badge() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + + assert "https://img.shields.io/badge/license-Apache--2.0-blue.svg" in readme + assert "https://img.shields.io/github/license/hashgraph-online/codex-plugin-scanner" not in readme + assert "publish-action-repo.yml" in readme + assert "docs/github-action-marketplace.md" not in readme