From 4e7268ebfa89eb019363228f64b700023a53ebfa Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 24 Aug 2026 03:55:51 +0900 Subject: [PATCH] ci: isolate firmware publisher credentials --- .github/workflows/publish-firmware.yml | 3 ++- README.md | 3 ++- docs/designs/firmware-supply-chain.md | 12 +++++++----- docs/plans/firmware-publisher.md | 6 +++--- tests/test_firmware.py | 9 +++++++++ 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-firmware.yml b/.github/workflows/publish-firmware.yml index 26035f9..73fa776 100644 --- a/.github/workflows/publish-firmware.yml +++ b/.github/workflows/publish-firmware.yml @@ -15,7 +15,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read concurrency: group: hana-firmware-publisher @@ -32,6 +32,7 @@ jobs: with: ref: main fetch-depth: 0 + ssh-key: ${{ secrets.HANA_FIRMWARE_PUBLISH_KEY }} - name: Set up Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 diff --git a/README.md b/README.md index 2d08cad..1b32a44 100644 --- a/README.md +++ b/README.md @@ -270,7 +270,8 @@ UI는 플랫폼 조건문을 갖지 않습니다. 수정합니다. `.hex`, 공개 board manifest, `registry.json`의 boards 항목은 사람이 수정할 수 없습니다. - 보드 소스 PR이 병합되면 고정된 Arduino toolchain을 사용하는 GitHub Actions가 - 스케치를 두 번 clean build하고 동일한 일반 HEX만 후속 커밋으로 게시합니다. + 스케치를 두 번 clean build하고 동일한 일반 HEX만 전용 Deploy Key를 통해 후속 + 커밋으로 게시합니다. 일반 workflow의 `GITHUB_TOKEN`은 읽기 전용입니다. - 앱 프로필 변경은 `registry.json`의 apps 항목과 대상 파일을 같은 PR에서 갱신합니다. - 기존 `id`의 의미를 바꾸지 않습니다. 호환되지 않는 변경은 새 `id` 또는 새 `schemaVersion`을 사용합니다. diff --git a/docs/designs/firmware-supply-chain.md b/docs/designs/firmware-supply-chain.md index 9d9948c..6ca0050 100644 --- a/docs/designs/firmware-supply-chain.md +++ b/docs/designs/firmware-supply-chain.md @@ -26,8 +26,9 @@ changes to them: Application profiles and the `apps` array remain human-managed. The protected `main` branch requires pull requests and validation, blocks deletion and force -pushes, and grants an always-on bypass only to the repository's GitHub Actions -App so the trusted publisher can add generated output. +pushes, and grants an always-on bypass only to a dedicated write-enabled deploy +key. The encrypted private key is available only to the trusted post-merge +publisher, while its `GITHUB_TOKEN` remains read-only. ## Authoring input @@ -74,9 +75,10 @@ sketch twice with the pinned toolchain to prove reproducible output. After an accepted source change reaches `main`, the trusted publisher checks out the latest `main`, performs the same two clean builds, compares the HEX bytes, regenerates all derived files, and commits them atomically as -`github-actions[bot]`. A bot push made with `GITHUB_TOKEN` does not recursively -start another publisher run. Concurrent publishers serialize and regenerate -from the latest `main` before pushing. +`github-actions[bot]`. The publisher pushes through its dedicated deploy key; +generated output paths are excluded from the workflow trigger so that commit +does not recursively start another publisher run. Concurrent publishers +serialize and regenerate from the latest `main` before pushing. Between the source merge and generated commit, existing boards remain on their previous valid HEX and newly added boards remain absent from `registry.json`. diff --git a/docs/plans/firmware-publisher.md b/docs/plans/firmware-publisher.md index ce1f99e..3beac02 100644 --- a/docs/plans/firmware-publisher.md +++ b/docs/plans/firmware-publisher.md @@ -4,7 +4,7 @@ **Goal:** Compile Hana Cloud Arduino sources into trusted Intel HEX artifacts and publish their manifests and registry hashes automatically. -**Architecture:** Fork pull requests can change authoring inputs but never generated firmware. A read-only validator compiles proposed sketches, while a post-merge GitHub Actions publisher with the sole branch-protection bypass performs two clean builds and atomically commits generated HEX, manifests, and registry board metadata. +**Architecture:** Fork pull requests can change authoring inputs but never generated firmware. A read-only validator compiles proposed sketches, while a post-merge GitHub Actions publisher using the sole write-enabled deploy-key bypass performs two clean builds and atomically commits generated HEX, manifests, and registry board metadata. **Tech Stack:** Python 3 standard library, Arduino CLI, Arduino AVR Core, JSON Schema 2020-12, GitHub Actions. @@ -105,12 +105,12 @@ - No additional source files unless verification reveals a defect. **Interfaces:** -- Consumes: merged workflow and GitHub Actions App integration. +- Consumes: merged workflow and the dedicated publisher deploy key. - Produces: published Uno HEX on `main` and an active main-branch ruleset. - [ ] **Step 1: Run all local verification**: unit tests, Python compilation, JSON parsing/schema checks, two-build HEX comparison, and `git diff --check`. - [ ] **Step 2: Open the Hana Cloud pull request and wait for the fork-safe validation check.** - [ ] **Step 3: Merge the PR using its exact head SHA.** - [ ] **Step 4: Observe the publisher bot commit and verify HEX size/hash, manifest hash, registry revision, and fresh-build byte equality.** -- [ ] **Step 5: Create an active `main` ruleset requiring pull requests and `validate`, blocking deletion/force pushes, and granting always-on bypass only to the GitHub Actions App.** +- [ ] **Step 5: Create an active `main` ruleset requiring pull requests, `validate`, and `guard-generated-files`, blocking deletion/force pushes, and granting always-on bypass only to the publisher deploy key.** - [ ] **Step 6: Audit the ruleset and repository tree, then record final SHAs and URLs.** diff --git a/tests/test_firmware.py b/tests/test_firmware.py index 2c9ff52..6a12a6f 100644 --- a/tests/test_firmware.py +++ b/tests/test_firmware.py @@ -276,6 +276,15 @@ def test_pip_cache_tracks_the_actual_dev_requirements_file(self) -> None: self.assertIn("cache: pip", workflow, name) self.assertIn("cache-dependency-path: requirements-dev.txt", workflow, name) + def test_publisher_uses_only_the_dedicated_deploy_key_for_writes(self) -> None: + workflow = ( + ROOT / ".github" / "workflows" / "publish-firmware.yml" + ).read_text(encoding="utf-8") + + self.assertIn("contents: read", workflow) + self.assertNotIn("contents: write", workflow) + self.assertIn("ssh-key: ${{ secrets.HANA_FIRMWARE_PUBLISH_KEY }}", workflow) + class PublishedSchemaTests(unittest.TestCase): def test_public_schema_accepts_the_compiled_firmware_contract(self) -> None: