name: Release on: push: tags: ['v*'] permissions: read-all env: REGISTRY: ghcr.io NORA: localhost:5000 IMAGE_NAME: ${{ github.repository }} jobs: build: name: Build & Push runs-on: [self-hosted, nora] outputs: hash: ${{ steps.hash.outputs.hash }} permissions: contents: read packages: write id-token: write # Sigstore cosign keyless signing steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Rust run: | echo "/home/github-runner/.cargo/bin" >> $GITHUB_PATH echo "RUSTUP_HOME=/home/github-runner/.rustup" >> $GITHUB_ENV echo "CARGO_HOME=/home/github-runner/.cargo" >> $GITHUB_ENV - name: Build release binary (musl static) run: | cargo build --release --target x86_64-unknown-linux-musl --package nora-registry cp target/x86_64-unknown-linux-musl/release/nora ./nora - name: Upload binary artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: nora-binary-${{ github.run_id }} path: ./nora retention-days: 1 - name: Compute binary hash for SLSA provenance id: hash run: | cp target/x86_64-unknown-linux-musl/release/nora ./nora-linux-amd64 sha256sum nora-linux-amd64 | base64 -w0 > hash.txt echo "hash=$(cat hash.txt)" >> $GITHUB_OUTPUT - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 with: driver-opts: network=host - name: Log in to GitHub Container Registry uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # ── Alpine ─────────────────────────────────────────────────────────────── - name: Extract metadata (alpine) id: meta-alpine uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest - name: Build and push (alpine) uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile platforms: linux/amd64 push: true tags: ${{ steps.meta-alpine.outputs.tags }} labels: ${{ steps.meta-alpine.outputs.labels }} # ── RED OS ─────────────────────────────────────────────────────────────── - name: Extract metadata (redos) id: meta-redos uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: suffix=-redos,onlatest=true tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest - name: Build and push (redos) uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile.redos platforms: linux/amd64 push: true tags: ${{ steps.meta-redos.outputs.tags }} labels: ${{ steps.meta-redos.outputs.labels }} # ── Astra Linux SE ─────────────────────────────────────────────────────── - name: Extract metadata (astra) id: meta-astra uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} flavor: suffix=-astra,onlatest=true tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=raw,value=latest - name: Build and push (astra) uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile.astra platforms: linux/amd64 push: true tags: ${{ steps.meta-astra.outputs.tags }} labels: ${{ steps.meta-astra.outputs.labels }} # ── Smoke test ────────────────────────────────────────────────────────── - name: Install cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v3 - name: Sign Docker images (keyless Sigstore) run: | TAGS=($(echo "${{ steps.meta-alpine.outputs.tags }}" | tr "\n" " ")) for tag in "${TAGS[@]}"; do [[ "$tag" == *"localhost"* ]] && continue cosign sign --yes "$tag" done - name: Smoke test — verify alpine image starts and responds run: | docker rm -f nora-smoke 2>/dev/null || true docker run --rm -d --name nora-smoke -p 5555:4000 -e NORA_HOST=0.0.0.0 ghcr.io/${{ github.repository }}:${{ steps.meta-alpine.outputs.version }} for i in $(seq 1 10); do curl -sf http://localhost:5555/health && break || sleep 2 done curl -sf http://localhost:5555/health docker stop nora-smoke scan: name: Scan (${{ matrix.name }}) runs-on: [self-hosted, nora] needs: build permissions: contents: read packages: read security-events: write strategy: fail-fast: false matrix: include: - name: alpine suffix: "" - name: redos suffix: "-redos" - name: astra suffix: "-astra" steps: - name: Set version tag (strip leading v) id: ver run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Trivy — image scan (${{ matrix.name }}) uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 with: scan-type: image image-ref: ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.tag }}${{ matrix.suffix }} format: sarif output: trivy-image-${{ matrix.name }}.sarif severity: HIGH,CRITICAL exit-code: 0 - name: Upload Trivy image results to GitHub Security tab uses: github/codeql-action/upload-sarif@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 # v4 if: always() with: sarif_file: trivy-image-${{ matrix.name }}.sarif category: trivy-image-${{ matrix.name }} provenance: name: SLSA Provenance needs: build permissions: actions: read id-token: write contents: write uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: "${{ needs.build.outputs.hash }}" upload-assets: true release: name: GitHub Release runs-on: [self-hosted, nora] needs: [build, scan, provenance] permissions: contents: write id-token: write # Sigstore cosign keyless signing packages: write # cosign needs push for signatures steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set version tag (strip leading v) id: ver run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Download binary artifact uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: nora-binary-${{ github.run_id }} path: ./artifacts - name: Prepare binary run: | cp ./artifacts/nora ./nora-linux-amd64 chmod +x ./nora-linux-amd64 sha256sum ./nora-linux-amd64 > nora-linux-amd64.sha256 echo "Binary size: $(du -sh nora-linux-amd64 | cut -f1)" cat nora-linux-amd64.sha256 - name: Generate SBOM (SPDX) uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0 with: format: spdx-json output-file: nora-${{ github.ref_name }}.sbom.spdx.json - name: Generate SBOM (CycloneDX) uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0 with: format: cyclonedx-json output-file: nora-${{ github.ref_name }}.sbom.cdx.json - name: Install cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v3 - name: Sign binary with cosign (keyless Sigstore) run: | cosign sign-blob --yes \ --output-signature nora-linux-amd64.sig \ --output-certificate nora-linux-amd64.cert \ --bundle nora-linux-amd64.bundle \ ./nora-linux-amd64 - name: Create Release uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: generate_release_notes: true files: | nora-linux-amd64 nora-linux-amd64.sha256 nora-linux-amd64.sig nora-linux-amd64.cert nora-linux-amd64.bundle nora-${{ github.ref_name }}.sbom.spdx.json nora-${{ github.ref_name }}.sbom.cdx.json body: | ## Install ```bash curl -fsSL https://getnora.io/install.sh | sh ``` Or download the binary directly: ```bash curl -LO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/nora-linux-amd64 chmod +x nora-linux-amd64 sudo mv nora-linux-amd64 /usr/local/bin/nora ``` ## Docker **Alpine (standard):** ```bash docker pull ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.tag }} ``` **RED OS:** ```bash docker pull ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.tag }}-redos ``` **Astra Linux SE:** ```bash docker pull ghcr.io/${{ github.repository }}:${{ steps.ver.outputs.tag }}-astra ``` ## Changelog See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md)