diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9da803d..ab9a702 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -63,6 +63,25 @@ on: required: false default: true type: boolean + secrets: + DOCKYARD_DEVELOPER_ID_APPLICATION: + description: Optional Dockyard Developer ID signing identity for release hooks + required: false + DOCKYARD_KEYCHAIN_ACCESS_GROUP: + description: Optional Dockyard keychain access group for release hooks + required: false + DOCKYARD_NOTARY_KEYCHAIN_PROFILE: + description: Optional Dockyard notarytool keychain profile for release hooks + required: false + DOCKYARD_SPARKLE_PRIVATE_KEY_FILE: + description: Optional Dockyard Sparkle private key file path for release hooks + required: false + DOCKYARD_SPARKLE_SIGN_UPDATE: + description: Optional Dockyard Sparkle sign_update executable path for release hooks + required: false + SPARKLE_FRAMEWORK_PATH: + description: Optional Sparkle framework path used by release hooks + required: false permissions: contents: write @@ -73,6 +92,13 @@ jobs: release: name: release runs-on: ${{ fromJSON(inputs.runs-on) }} + env: + DOCKYARD_DEVELOPER_ID_APPLICATION: ${{ secrets.DOCKYARD_DEVELOPER_ID_APPLICATION }} + DOCKYARD_KEYCHAIN_ACCESS_GROUP: ${{ secrets.DOCKYARD_KEYCHAIN_ACCESS_GROUP }} + DOCKYARD_NOTARY_KEYCHAIN_PROFILE: ${{ secrets.DOCKYARD_NOTARY_KEYCHAIN_PROFILE }} + DOCKYARD_SPARKLE_PRIVATE_KEY_FILE: ${{ secrets.DOCKYARD_SPARKLE_PRIVATE_KEY_FILE }} + DOCKYARD_SPARKLE_SIGN_UPDATE: ${{ secrets.DOCKYARD_SPARKLE_SIGN_UPDATE }} + SPARKLE_FRAMEWORK_PATH: ${{ secrets.SPARKLE_FRAMEWORK_PATH }} steps: - uses: actions/checkout@v4 with: @@ -158,6 +184,8 @@ jobs: echo "No executable release artifact build hook is configured: ${{ inputs.build-script }}" fi - name: Publish release artifacts + env: + GH_TOKEN: ${{ github.token }} run: | if [[ ! -x "${{ inputs.publish-script }}" ]]; then echo "Missing executable publish script: ${{ inputs.publish-script }}" >&2 diff --git a/docs/bootstrap/tier-a-ci-contract.md b/docs/bootstrap/tier-a-ci-contract.md index 25aa43f..d2f571e 100644 --- a/docs/bootstrap/tier-a-ci-contract.md +++ b/docs/bootstrap/tier-a-ci-contract.md @@ -69,6 +69,7 @@ on: jobs: release: uses: OMT-Global/bootstrap/.github/workflows/release.yml@refs/heads/main + secrets: inherit with: runs-on: '["ubuntu-latest"]' verify-script: scripts/ci/run-release-verification.sh @@ -83,6 +84,14 @@ jobs: update-minor-tag: true ``` +Release hooks that need signing, notarization, or external publish credentials +must read them from environment variables. The reusable release workflow passes +declared caller secrets through to hook scripts, and the caller must include +`secrets: inherit` or explicitly map those values. For macOS app releases this +includes Dockyard-compatible variables such as +`DOCKYARD_DEVELOPER_ID_APPLICATION`, `DOCKYARD_KEYCHAIN_ACCESS_GROUP`, +`DOCKYARD_NOTARY_KEYCHAIN_PROFILE`, and optional Sparkle paths. + Release policy: - create immutable exact tags such as `v1.2.3` diff --git a/tests/reusable-workflows.test.ts b/tests/reusable-workflows.test.ts index b5a0ad9..b72dca8 100644 --- a/tests/reusable-workflows.test.ts +++ b/tests/reusable-workflows.test.ts @@ -30,8 +30,19 @@ describe("reusable workflows", () => { expect((workflow.on as any).workflow_call.inputs["artifact-dir"].default).toBe("dist/release"); expect((workflow.on as any).workflow_call.inputs["tag-prefix"].default).toBe("v"); expect((workflow.on as any).workflow_call.inputs["update-major-tag"].default).toBe(true); + const releaseSecrets = (workflow.on as any).workflow_call.secrets; + expect(releaseSecrets.DOCKYARD_DEVELOPER_ID_APPLICATION.required).toBe(false); + expect(releaseSecrets.DOCKYARD_KEYCHAIN_ACCESS_GROUP.required).toBe(false); + expect(releaseSecrets.DOCKYARD_NOTARY_KEYCHAIN_PROFILE.required).toBe(false); const releaseJob = (workflow.jobs as any).release; expect(releaseJob).toBeTruthy(); + expect(releaseJob.env.DOCKYARD_DEVELOPER_ID_APPLICATION).toBe( + "${{ secrets.DOCKYARD_DEVELOPER_ID_APPLICATION }}" + ); + expect(releaseJob.env.DOCKYARD_KEYCHAIN_ACCESS_GROUP).toBe("${{ secrets.DOCKYARD_KEYCHAIN_ACCESS_GROUP }}"); + expect(releaseJob.env.DOCKYARD_NOTARY_KEYCHAIN_PROFILE).toBe( + "${{ secrets.DOCKYARD_NOTARY_KEYCHAIN_PROFILE }}" + ); const stepNames = releaseJob.steps.map((step: any) => step.name).filter(Boolean); expect(stepNames).toEqual([ "Derive release metadata", @@ -44,9 +55,11 @@ describe("reusable workflows", () => { "Promote floating SemVer tags" ]); const deriveMetadata = releaseJob.steps.find((step: any) => step.name === "Derive release metadata"); + const publishArtifacts = releaseJob.steps.find((step: any) => step.name === "Publish release artifacts"); const createRelease = releaseJob.steps.find((step: any) => step.name === "Create GitHub release"); const promoteTags = releaseJob.steps.find((step: any) => step.name === "Promote floating SemVer tags"); expect(deriveMetadata.run).toContain("semver_component='(0|[1-9][0-9]*)'"); + expect(publishArtifacts.env.GH_TOKEN).toBe("${{ github.token }}"); expect(deriveMetadata.run).toContain("prerelease_identifier="); expect(deriveMetadata.run).toContain("is_prerelease=${is_prerelease}"); expect(createRelease.run).toContain("release_create_args=(--prerelease --latest=false)");