Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/publish-firmware.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ on:
workflow_dispatch:

permissions:
contents: write
contents: read

concurrency:
group: hana-firmware-publisher
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`을 사용합니다.
Expand Down
12 changes: 7 additions & 5 deletions docs/designs/firmware-supply-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.
Expand Down
6 changes: 3 additions & 3 deletions docs/plans/firmware-publisher.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.**
9 changes: 9 additions & 0 deletions tests/test_firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading