-
Notifications
You must be signed in to change notification settings - Fork 3
Adding publish-rpm-repository action/workflow #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| paths: | ||
| .github/workflows/publish-rpm-repository.yml: | ||
| ignore: | ||
| - 'property "workflow_repository" is not defined in object type' | ||
| - 'property "workflow_sha" is not defined in object type' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| # SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| name: Publish RPM repository | ||
| run-name: Publish RPM repository for ${{ github.ref }} | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| artifact-name-signed-rpms: | ||
| description: 'Name of the signed RPM artifact to publish' | ||
| type: string | ||
| default: 'rpms-signed' | ||
| required: false | ||
| artifact-name-package-signing-keys: | ||
| description: 'Name of the certified ephemeral package public-key artifact' | ||
| type: string | ||
| default: 'public-keys' | ||
| required: false | ||
|
Comment on lines
+15
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ibid, see above grammar comment. |
||
| repository-path: | ||
| description: 'Path below the bucket, for example stable/el9/x86_64' | ||
| type: string | ||
| required: true | ||
| s3-bucket: | ||
| description: 'Destination S3 bucket' | ||
| type: string | ||
| required: true | ||
| aws-region: | ||
| description: 'AWS region containing the destination bucket' | ||
| type: string | ||
| required: true | ||
| aws-role-arn: | ||
| description: 'GitHub OIDC publisher role ARN' | ||
| type: string | ||
| required: true | ||
| aws-account-id: | ||
| description: 'Expected AWS account ID for confused-deputy protection' | ||
| type: string | ||
| required: true | ||
| cloudfront-distribution-id: | ||
| description: 'CloudFront distribution to invalidate after publication' | ||
| type: string | ||
| required: false | ||
| public-base-url: | ||
| description: 'Public repository base URL, for example https://rpm.openchami.org' | ||
| type: string | ||
| required: false | ||
| metadata-signing-key-fingerprint: | ||
| description: 'Full fingerprint of the durable repository metadata signing key' | ||
| type: string | ||
| required: true | ||
| package-master-fingerprint: | ||
| description: 'Full fingerprint of the trusted package-signing master key' | ||
| type: string | ||
| required: true | ||
| repository-key-filename: | ||
| description: 'Published filename for the metadata signing public key' | ||
| type: string | ||
| default: 'RPM-GPG-KEY-OpenCHAMI' | ||
| required: false | ||
| deployment-environment: | ||
| description: 'GitHub Environment containing publication protections and key secrets' | ||
| type: string | ||
| default: 'rpm-publish' | ||
| required: false | ||
| dry-run: | ||
| description: 'Build and verify repository metadata without accessing AWS' | ||
| type: boolean | ||
| default: false | ||
| required: false | ||
| outputs: | ||
| repository-url: | ||
| description: 'Published repository URL' | ||
| value: ${{ jobs.publish.outputs.repository-url }} | ||
| package-count: | ||
| description: 'Number of packages in the generated repository' | ||
| value: ${{ jobs.publish.outputs.package-count }} | ||
| repomd-sha256: | ||
| description: 'SHA-256 digest of the committed repomd.xml' | ||
| value: ${{ jobs.publish.outputs.repomd-sha256 }} | ||
| invalidation-id: | ||
| description: 'CloudFront invalidation ID, when requested' | ||
| value: ${{ jobs.publish.outputs.invalidation-id }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
|
|
||
| concurrency: | ||
| group: rpm-repository-${{ inputs.s3-bucket }}-${{ inputs.repository-path }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| publish: | ||
| runs-on: ubuntu-latest | ||
| environment: ${{ inputs.deployment-environment }} | ||
| outputs: | ||
| repository-url: ${{ steps.publish.outputs.repository-url }} | ||
| package-count: ${{ steps.publish.outputs.package-count }} | ||
| repomd-sha256: ${{ steps.publish.outputs.repomd-sha256 }} | ||
| invalidation-id: ${{ steps.publish.outputs.invalidation-id }} | ||
| steps: | ||
| - name: Checkout publisher action at the reusable workflow commit | ||
| uses: actions/checkout@9f698171ed81b15d1823a05fc7211befd50c8ae0 # v6.0.3 | ||
| with: | ||
|
Comment on lines
+102
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we are using the full ref associated with the tag and not just the tag itself? This repo doesn't appear to have a standard (some tags, some full refs). Out of scope, but should we standardize later on? If so, I'll create a tracking issue. |
||
| repository: ${{ job.workflow_repository }} | ||
| ref: ${{ job.workflow_sha }} | ||
| path: .openchami-actions | ||
| persist-credentials: false | ||
|
|
||
| - name: Download signed RPM artifacts | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | ||
| with: | ||
| name: ${{ inputs.artifact-name-signed-rpms }} | ||
| path: dist/rpms | ||
|
|
||
| - name: Download package signing keys | ||
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | ||
| with: | ||
| name: ${{ inputs.artifact-name-package-signing-keys }} | ||
| path: dist/package-keys | ||
|
|
||
| - name: Configure AWS credentials | ||
| if: ${{ !inputs.dry-run }} | ||
| uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 | ||
| with: | ||
| aws-region: ${{ inputs.aws-region }} | ||
| role-to-assume: ${{ inputs.aws-role-arn }} | ||
| role-session-name: OpenCHAMI-RPM-${{ github.run_id }} | ||
| allowed-account-ids: ${{ inputs.aws-account-id }} | ||
| unset-current-credentials: true | ||
|
|
||
| - name: Publish repository | ||
| id: publish | ||
| uses: ./.openchami-actions/actions/publish-rpm-repository | ||
| with: | ||
| rpm-path: dist/rpms | ||
| repository-path: ${{ inputs.repository-path }} | ||
| s3-bucket: ${{ inputs.s3-bucket }} | ||
| cloudfront-distribution-id: ${{ inputs.cloudfront-distribution-id }} | ||
| public-base-url: ${{ inputs.public-base-url }} | ||
| package-signing-keys-path: dist/package-keys | ||
| package-master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} | ||
| package-master-fingerprint: ${{ inputs.package-master-fingerprint }} | ||
| authorized-package-repo-fingerprints: ${{ vars.PACKAGE_REPO_CERT_FINGERPRINTS }} | ||
| metadata-signing-key-armored-b64: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_B64 }} | ||
| metadata-signing-key-fingerprint: ${{ inputs.metadata-signing-key-fingerprint }} | ||
| metadata-signing-key-passphrase: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE }} | ||
| repository-key-filename: ${{ inputs.repository-key-filename }} | ||
| dry-run: ${{ inputs.dry-run }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,13 +14,15 @@ Reusable GitHub Actions for CI/CD. | |
| - `actions/gpg-sign-rpm`: RPM signing with ephemeral keys | ||
| - `actions/gpg-check-key-expiration`: Fails CI if a signing key is expired or expiring soon | ||
| - `actions/gpg-verify-trust-chain`: Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs | ||
| - `actions/publish-rpm-repository`: Generates durable-key-signed repository metadata and safely publishes RPM repositories to S3 | ||
| - `.github/workflows/go-build-release.yml`: Reusable workflow for GoReleaser builds | ||
| - `.github/workflows/docker-build-release.yml`: Reusable workflow for multi-arch container image builds | ||
| - `.github/workflows/build-publish-container-goreleaser.yml`: Builds and publishes a container image via GoReleaser | ||
| - `.github/workflows/build-rpm-quadlet.yml`: Builds a caller repo's podman quadlet RPM | ||
| - `.github/workflows/gpg-sign-artifacts.yml`: Signs unsigned RPM artifacts with a per-run ephemeral key | ||
| - `.github/workflows/validate-rpm-quadlet.yml`: Validates a signed quadlet RPM's installed file list | ||
| - `.github/workflows/release-signed-artifacts.yml`: Publishes a GitHub Release with signed RPMs and public keys | ||
| - `.github/workflows/publish-rpm-repository.yml`: Publishes aggregated RPMs through S3 and CloudFront using GitHub OIDC | ||
|
Comment on lines
+17
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: it's a bit jarring for both the workflow and the action to have the same name. |
||
| - `.github/workflows/lint-workflows.yml`: Reusable workflow that lints workflow files (actionlint + zizmor) | ||
| - `.github/workflows/govulncheck.yml`: Reusable workflow that scans Go modules for known CVEs | ||
| - `.github/workflows/dependency-review.yml`: Reusable workflow that gates PRs introducing CVE-flagged deps | ||
|
|
@@ -211,6 +213,36 @@ jobs: | |
| uses: OpenCHAMI/github-actions/.github/workflows/release-signed-artifacts.yml@v3.5 | ||
| ``` | ||
|
|
||
| ### publish-rpm-repository (Reusable Workflow) | ||
|
|
||
| Aggregates signed RPM artifacts into a DNF/YUM repository, signs `repomd.xml` | ||
| with a durable repository metadata key, publishes to S3 using GitHub OIDC, and | ||
| invalidates only mutable CloudFront metadata paths. The calling repository's | ||
| `rpm-publish` GitHub Environment supplies the durable signing-key secrets and | ||
| can enforce required reviewers. | ||
|
|
||
| ```yaml | ||
| jobs: | ||
| publish: | ||
| uses: OpenCHAMI/github-actions/.github/workflows/publish-rpm-repository.yml@v3.6 | ||
| with: | ||
| repository-path: stable/el9/x86_64 | ||
| s3-bucket: ${{ vars.RPM_REPOSITORY_BUCKET }} | ||
| aws-region: us-east-1 | ||
| aws-role-arn: ${{ vars.RPM_REPOSITORY_PUBLISHER_ROLE_ARN }} | ||
| aws-account-id: ${{ vars.AWS_ACCOUNT_ID }} | ||
| cloudfront-distribution-id: ${{ vars.RPM_REPOSITORY_DISTRIBUTION_ID }} | ||
| public-base-url: https://rpm.openchami.org | ||
| package-master-fingerprint: ${{ vars.MASTER_FPR }} | ||
| metadata-signing-key-fingerprint: ${{ vars.RPM_REPOSITORY_SIGNING_KEY_FINGERPRINT }} | ||
|
Comment on lines
+229
to
+237
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reminder for @seantronsen We might want to create a github issue that focuses on standardizing varnames + usage across the recently created workflows + actions. This is out of scope for this PR, but if it's to happen it should occur before these are used in the many downstream repos under the org. |
||
| ``` | ||
|
|
||
| The service repositories continue signing RPM packages with certified ephemeral | ||
| keys. Publication verifies those chains and emits a rotating DNF package-key | ||
| bundle. Only repository metadata uses the durable key. See the | ||
| [action documentation](actions/publish-rpm-repository/README.md) for publication | ||
| ordering, key handling, and client trust requirements. | ||
|
|
||
| ## Actions | ||
|
|
||
| ### gpg-ephemeral-key (Deprecated - use gpg-configure-release-keys) | ||
|
|
@@ -228,6 +260,12 @@ Fails CI if the provided signing key is expired or expiring within a threshold. | |
| ### gpg-verify-trust-chain | ||
| Verifies the master/repo-cert/ephemeral trust chain and optionally checksigs RPMs. See the [action README](actions/gpg-verify-trust-chain/README.md). | ||
|
|
||
| ### publish-rpm-repository | ||
|
|
||
| Builds repository metadata from signed RPMs, signs `repomd.xml` with the durable | ||
| repository metadata key, and publishes immutable packages before the metadata | ||
| commit point. See the [action README](actions/publish-rpm-repository/README.md). | ||
|
|
||
| ## Security Model | ||
|
|
||
| Trust chain: `Ephemeral Key <- Repo Cert Key <- Offline Master Key`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: 2026 OpenCHAMI a Series of LF Projects, LLC | ||
| SPDX-License-Identifier: MIT | ||
| --> | ||
|
|
||
| # Publish RPM Repository Action | ||
|
|
||
| Builds DNF/YUM repository metadata for signed RPMs, signs `repomd.xml` with a | ||
| durable repository metadata key, and safely publishes the repository to S3. | ||
|
|
||
| Each service repository signs its RPMs with a certified per-run ephemeral key | ||
| before the release workflow aggregates them. This action authenticates every | ||
| ephemeral key through the trusted master and repository certification-key chain, | ||
| then verifies every incoming and previously published RPM before signing the | ||
| repository metadata. | ||
|
|
||
| ## Publication model | ||
|
|
||
| The action restores previously published immutable RPMs, adds the incoming RPMs, | ||
| and regenerates metadata for the complete package set. It never deletes packages | ||
| or metadata from S3. Publication occurs in this order: | ||
|
|
||
| 1. Immutable RPMs under `Packages/`. | ||
| 2. Content-addressed files under `repodata/`. | ||
| 3. Immutable authenticated package-key archives. | ||
| 4. Rotating package-key bundle and durable metadata signing public key. | ||
| 5. `repodata/repomd.xml.asc`. | ||
| 6. `repodata/repomd.xml` as the final metadata pointer. | ||
| 7. A CloudFront invalidation limited to metadata and rotating public keys. | ||
|
|
||
| S3 cannot atomically replace `repomd.xml` and its detached signature. Publishing | ||
| the signature first makes the short update interval fail closed: a client may | ||
| temporarily reject mismatched metadata and retry, but it cannot accept unsigned | ||
| or partially published metadata. | ||
|
|
||
| The reusable workflow serializes publishers for a repository path. Production | ||
| publication should only be invoked by the central `OpenCHAMI/release` repository | ||
| so publishers in separate repositories cannot race. | ||
|
|
||
| ## Inputs | ||
|
|
||
| | Input | Required | Default | Description | | ||
| |---|---:|---|---| | ||
| | `rpm-path` | Yes | | File or directory containing signed RPMs | | ||
| | `repository-path` | Yes | | Bucket prefix such as `stable/el9/x86_64` | | ||
| | `s3-bucket` | For publication | | Destination bucket | | ||
| | `cloudfront-distribution-id` | No | | Distribution to invalidate | | ||
| | `public-base-url` | No | | Public base URL such as `https://rpm.openchami.org` | | ||
| | `package-signing-keys-path` | Yes | | Directory containing paired `repo-cert.pub.asc` and `ephemeral.pub.asc` files | | ||
| | `package-master-public-key-asc` | Yes | | Trusted package-signing master public key | | ||
| | `package-master-fingerprint` | Yes | | Full trusted package-signing master fingerprint | | ||
| | `authorized-package-repo-fingerprints` | Yes | | Current repo-cert fingerprints allowed to certify new ephemeral keys | | ||
| | `package-key-bundle-filename` | No | `RPM-GPG-KEY-OpenCHAMI-Packages` | Rotating authenticated package-key bundle | | ||
| | `metadata-signing-key-armored-b64` | Yes | | Base64-encoded armored durable secret key | | ||
| | `metadata-signing-key-fingerprint` | Yes | | Full durable signing-key fingerprint | | ||
| | `metadata-signing-key-passphrase` | No | | Secret-key passphrase | | ||
| | `repository-key-filename` | No | `RPM-GPG-KEY-OpenCHAMI` | Public key filename | | ||
| | `require-signed-rpms` | No | `true` | Reject RPMs without package signatures | | ||
| | `dry-run` | No | `false` | Build and verify locally without AWS | | ||
|
|
||
| ## Outputs | ||
|
|
||
| | Output | Description | | ||
| |---|---| | ||
| | `repository-directory` | Local complete repository directory | | ||
| | `repository-url` | Public URL, S3 URL, or dry-run file URL | | ||
| | `package-count` | Number of RPMs in the generated repository | | ||
| | `repomd-sha256` | SHA-256 digest of `repomd.xml` | | ||
| | `invalidation-id` | CloudFront invalidation ID when requested | | ||
|
|
||
| ## Direct usage | ||
|
|
||
| The caller must configure short-lived AWS credentials before invoking the action. | ||
|
|
||
| ```yaml | ||
| permissions: | ||
| contents: read | ||
| id-token: write | ||
|
|
||
| steps: | ||
| - uses: aws-actions/configure-aws-credentials@v6.2.3 | ||
| with: | ||
| aws-region: us-east-1 | ||
| role-to-assume: ${{ vars.RPM_REPOSITORY_PUBLISHER_ROLE_ARN }} | ||
| allowed-account-ids: ${{ vars.AWS_ACCOUNT_ID }} | ||
|
|
||
| - uses: OpenCHAMI/github-actions/actions/publish-rpm-repository@v3.6 | ||
| with: | ||
| rpm-path: dist/rpms | ||
| repository-path: stable/el9/x86_64 | ||
| s3-bucket: ${{ vars.RPM_REPOSITORY_BUCKET }} | ||
| cloudfront-distribution-id: ${{ vars.RPM_REPOSITORY_DISTRIBUTION_ID }} | ||
| public-base-url: https://rpm.openchami.org | ||
| package-signing-keys-path: dist/package-keys | ||
| package-master-public-key-asc: ${{ secrets.MASTER_PUBLIC_ASC }} | ||
| package-master-fingerprint: ${{ vars.MASTER_FPR }} | ||
| authorized-package-repo-fingerprints: ${{ vars.PACKAGE_REPO_CERT_FINGERPRINTS }} | ||
| metadata-signing-key-armored-b64: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_B64 }} | ||
| metadata-signing-key-fingerprint: ${{ vars.RPM_REPOSITORY_SIGNING_KEY_FINGERPRINT }} | ||
| metadata-signing-key-passphrase: ${{ secrets.RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE }} | ||
| ``` | ||
|
|
||
| For production, prefer the repository's `publish-rpm-repository.yml` reusable | ||
| workflow, which supplies OIDC permissions, GitHub Environment protections, and | ||
| concurrency controls. | ||
|
|
||
| ## Durable key handling | ||
|
|
||
| Store these secrets in the protected `rpm-publish` GitHub Environment of the | ||
| calling repository: | ||
|
|
||
| - `RPM_REPOSITORY_SIGNING_KEY_B64` | ||
| - `RPM_REPOSITORY_SIGNING_KEY_PASSPHRASE` | ||
| - `MASTER_PUBLIC_ASC` | ||
|
|
||
| Store the non-secret fingerprint and AWS identifiers as environment variables. | ||
| The action imports the durable secret key into an isolated temporary | ||
| `GNUPGHOME`, exports only its public key into the repository, and shreds the | ||
| temporary keyring at exit. | ||
|
|
||
| `PACKAGE_REPO_CERT_FINGERPRINTS` is a comma, whitespace, or newline-separated | ||
| allowlist maintained in the protected environment. Historical repo-cert keys | ||
| remain archived so old RPM signatures can be verified, but only keys in this | ||
| current allowlist may certify newly submitted ephemeral package keys. Remove a | ||
| retired or compromised repo-cert fingerprint before the next publication. | ||
|
|
||
| Clients should configure both `gpgcheck=1` for ephemeral package signatures and | ||
| `repo_gpgcheck=1` for the durable `repomd.xml` signature. The `.repo` file must | ||
| list both published keys: | ||
|
|
||
| ```ini | ||
| gpgkey=https://rpm.openchami.org/stable/el9/x86_64/RPM-GPG-KEY-OpenCHAMI-Packages | ||
| https://rpm.openchami.org/stable/el9/x86_64/RPM-GPG-KEY-OpenCHAMI | ||
| ``` | ||
|
|
||
| The package bundle contains only ephemeral public keys whose certification | ||
| chains were verified during publication. DNF imports those concrete keys rather | ||
| than being expected to discover the OpenPGP certification chain itself. | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar mismatch. Var name is plural, doc comment is singular.