diff --git a/.github/workflows/release-discussion.yaml b/.github/workflows/release-discussion.yaml index 8e9ea7d..a1cb045 100644 --- a/.github/workflows/release-discussion.yaml +++ b/.github/workflows/release-discussion.yaml @@ -1,5 +1,5 @@ --- -name: "Release Discussion" +name: "Release Discussion (Deprecated)" on: workflow_call: inputs: @@ -16,37 +16,17 @@ on: required: true discussion-category-id: required: true -permissions: - contents: read + +permissions: {} + jobs: - create_discussion: + deprecated: runs-on: ubuntu-latest - permissions: - contents: read - discussions: write - env: - DISCUSSION_REPOSITORY_ID: ${{ secrets.discussion-repository-id }} - DISCUSSION_CATEGORY_ID: ${{ secrets.discussion-category-id }} + permissions: {} steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 - with: - egress-policy: audit - - name: Check for Discussion Repository ID - if: ${{ env.DISCUSSION_REPOSITORY_ID == '' }} + - name: Deprecation notice run: | - echo "discussion-repository-id secret is not set" + echo "::error::This workflow (release-discussion.yaml) has been deprecated and consolidated into release.yaml." + echo "::error::Migrate to the consolidated release workflow by setting the 'discussion-category-id' and 'discussion-repository-id' secrets on release.yaml instead." + echo "::error::See migration guide: https://github.com/github-community-projects/ospo-reusable-workflows/blob/main/docs/release-discussion.md" exit 1 - - name: Check for Discussion Category ID - if: ${{ env.DISCUSSION_CATEGORY_ID == '' }} - run: | - echo "discussion-category-id secret is not set" - exit 1 - - name: Create an Announcement Discussion for Release - uses: abirismyname/create-discussion@c2b7c825241769dda523865ae444a879f6bbd0e0 - with: - title: ${{ inputs.full-tag }} - body: ${{ inputs.body }} - repository-id: ${{ env.DISCUSSION_REPOSITORY_ID }} - category-id: ${{ env.DISCUSSION_CATEGORY_ID }} - github-token: ${{ secrets.github-token }} diff --git a/.github/workflows/release-image.yaml b/.github/workflows/release-image.yaml index 703aac8..5805693 100644 --- a/.github/workflows/release-image.yaml +++ b/.github/workflows/release-image.yaml @@ -1,5 +1,5 @@ --- -name: "Release Image" +name: "Release Image (Deprecated)" on: workflow_call: inputs: @@ -25,56 +25,17 @@ on: required: true image-registry-password: required: true -permissions: - contents: read + +permissions: {} + jobs: - create_action_images: + deprecated: runs-on: ubuntu-latest - permissions: - contents: read - packages: write - id-token: write - attestations: write - env: - IMAGE_REGISTRY: ${{ secrets.image-registry }} - IMAGE_REGISTRY_USERNAME: ${{ secrets.image-registry-username }} - IMAGE_REGISTRY_PASSWORD: ${{ secrets.image-registry-password }} + permissions: {} steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 - with: - egress-policy: audit - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd - - name: Log in to the Container registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 - with: - registry: ${{ env.IMAGE_REGISTRY }} - username: ${{ env.IMAGE_REGISTRY_USERNAME }} - password: ${{ env.IMAGE_REGISTRY_PASSWORD}} - - name: Push Docker Image - if: ${{ success() }} - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f - id: push - with: - context: . - file: ./Dockerfile - push: true - tags: | - ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:latest - ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:${{ inputs.full-tag }} - ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:${{ inputs.short-tag }} - platforms: linux/amd64,linux/arm64 - provenance: false - sbom: false - - name: Generate artifact attestation - if: ${{ inputs.create-attestation }} - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-name: ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name}} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true - github-token: ${{ secrets.github-token }} + - name: Deprecation notice + run: | + echo "::error::This workflow (release-image.yaml) has been deprecated and consolidated into release.yaml." + echo "::error::Migrate to the consolidated release workflow by setting the 'image-name' input on release.yaml instead." + echo "::error::See migration guide: https://github.com/github-community-projects/ospo-reusable-workflows/blob/main/docs/release-image.md" + exit 1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 76571c4..7bf0fc6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,19 +4,62 @@ on: workflow_call: inputs: publish: + description: "Publish the release after all jobs complete. When false, the release remains a draft." required: false type: boolean default: true release-config-name: required: true type: string - update-major-tag: + image-name: + description: "Docker image name (e.g., owner/repo). Enables image build/push when set." + required: false + type: string + default: "" + create-attestation: + description: "Create build provenance attestation for artifacts and/or Docker images." required: false type: boolean - default: true + default: false + create-discussion: + description: "Create discussion about release" + required: false + type: boolean + default: false + image-registry: + description: "Container registry URL." + required: false + type: string + default: "ghcr.io" + image-registry-username: + description: "Container registry username." + required: false + type: string + default: "" + image-platforms: + description: "Comma-separated list of target platforms for Docker build." + required: false + type: string + default: "linux/amd64,linux/arm64" + goreleaser-config-path: + description: "Path to GoReleaser config file. Enables GoReleaser build/upload when set." + required: false + type: string + default: "" + go-version-file: + description: "Path to go.mod or go.work file for Go version detection. Used by the GoReleaser job." + required: false + type: string + default: "go.mod" secrets: github-token: required: true + image-registry-password: + required: false + discussion-category-id: + required: false + discussion-repository-id: + required: false outputs: full-tag: description: "Full tag of release" @@ -27,8 +70,9 @@ on: body: description: "Body content of release" value: ${{ jobs.create_release.outputs.body }} -permissions: - contents: read + +permissions: {} + jobs: create_release: # Release if: @@ -46,50 +90,284 @@ jobs: full-tag: ${{ steps.release-drafter.outputs.tag_name }} short-tag: ${{ steps.get_tag_name.outputs.SHORT_TAG }} body: ${{ steps.release-drafter.outputs.body }} + release-id: ${{ steps.release-drafter.outputs.id }} runs-on: ubuntu-latest permissions: - contents: write - pull-requests: read + contents: write # Create draft release via release-drafter + pull-requests: read # Read PR labels for release-drafter steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 with: egress-policy: audit - - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: true # Required for git push of tags + - name: Draft release id: release-drafter + uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 with: config-name: ${{ inputs.release-config-name }} - publish: ${{ inputs.publish }} + publish: false token: ${{ secrets.github-token }} - name: Get the Short Tag id: get_tag_name run: | - short_tag=$(echo ${{ steps.release-drafter.outputs.tag_name }} | cut -d. -f1) + short_tag=$(echo "${{ steps.release-drafter.outputs.tag_name }}" | cut -d. -f1) echo "SHORT_TAG=$short_tag" >> "$GITHUB_OUTPUT" + - name: Create and push tags + run: | + git tag -f "${{ steps.release-drafter.outputs.tag_name }}" + git tag -f "${{ steps.get_tag_name.outputs.SHORT_TAG }}" "${{ steps.release-drafter.outputs.tag_name }}" + git push -f origin "${{ steps.release-drafter.outputs.tag_name }}" + git push -f origin "${{ steps.get_tag_name.outputs.SHORT_TAG }}" - update_major_tag: + release_goreleaser: needs: create_release - if: ${{ inputs.update-major-tag && needs.create_release.outputs.full-tag != '' }} + if: ${{ inputs.goreleaser-config-path != '' && needs.create_release.outputs.full-tag != '' }} runs-on: ubuntu-latest permissions: - contents: write + contents: write # Upload release assets + id-token: write # Federate for artifact attestation + attestations: write # Generate artifact attestations steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - - name: Checkout Repo + - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - fetch-tags: true + fetch-depth: 0 + persist-credentials: false + + - name: Install yq + uses: mikefarah/yq@0f4fb8d35ec1a939d78dd6862f494d19ec589f19 # v4.52.5 + with: + cmd: echo "yq installed" + + - name: Validate GoReleaser config has release disabled + run: | + config="${{ inputs.goreleaser-config-path }}" + if ! [ -f "$config" ]; then + echo "::error::GoReleaser config file not found: $config" + exit 1 + fi + release_disabled=$(yq '.release.disable' "$config") + if [ "$release_disabled" != "true" ]; then + echo "::error::GoReleaser config must have 'release: disable: true' to prevent conflicting with the draft release created by this workflow. See https://github.com/github-community-projects/ospo-reusable-workflows/blob/main/docs/release.md#goreleaser-configuration" + exit 1 + fi + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: ${{ inputs.go-version-file }} + + - name: Build with GoReleaser + uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean --config ${{ inputs.goreleaser-config-path }} + env: + GORELEASER_CURRENT_TAG: ${{ needs.create_release.outputs.full-tag }} + + - name: Upload artifacts to draft release + run: | + shopt -s nullglob + files=(dist/*.tar.gz dist/*.zip dist/checksums.txt) + if [ ${#files[@]} -eq 0 ]; then + echo "::error::No artifacts found in dist/ to upload" + exit 1 + fi + gh release upload "${{ needs.create_release.outputs.full-tag }}" \ + "${files[@]}" \ + --clobber + env: + GH_TOKEN: ${{ secrets.github-token }} + + - name: Check repository visibility + id: repo-visibility + run: | + visibility=$(gh api "repos/${{ github.repository }}" --jq '.visibility') + echo "is_public=$( [ "$visibility" = "public" ] && echo true || echo false )" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.github-token }} + + - name: Generate artifact attestation + if: ${{ inputs.create-attestation && steps.repo-visibility.outputs.is_public == 'true' }} + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: | + dist/*.tar.gz + dist/*.zip + dist/checksums.txt + + - name: Skip attestation notice + if: ${{ inputs.create-attestation && steps.repo-visibility.outputs.is_public != 'true' }} + run: | + echo "::warning::Artifact attestation skipped — not available for private user-owned repositories. Make this repository public to enable attestation." + + release_image: + needs: create_release + if: ${{ inputs.image-name != '' && needs.create_release.outputs.full-tag != '' }} + runs-on: ubuntu-latest + permissions: + contents: read # Clone the repository + packages: write # Push container images + id-token: write # Federate via Workload Identity for attestation + attestations: write # Create build provenance attestation + env: + IMAGE_REGISTRY: ${{ inputs.image-registry }} + IMAGE_REGISTRY_USERNAME: ${{ inputs.image-registry-username || github.actor }} + IMAGE_REGISTRY_PASSWORD: ${{ secrets.image-registry-password }} + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Validate image registry password + if: ${{ env.IMAGE_REGISTRY_PASSWORD == '' }} + run: | + echo "::error::image-registry-password secret is required when image-name is set" + exit 1 + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: ref: ${{ needs.create_release.outputs.full-tag }} - persist-credentials: true + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + + - name: Log in to the Container registry + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 + with: + registry: ${{ env.IMAGE_REGISTRY }} + username: ${{ env.IMAGE_REGISTRY_USERNAME }} + password: ${{ env.IMAGE_REGISTRY_PASSWORD }} + + - name: Push Docker Image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + id: push + with: + context: . + file: ./Dockerfile + push: true + tags: | + ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:latest + ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:${{ needs.create_release.outputs.full-tag }} + ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }}:${{ needs.create_release.outputs.short-tag }} + platforms: ${{ inputs.image-platforms }} + provenance: false + sbom: false + + - name: Check repository visibility + if: ${{ inputs.create-attestation }} + id: repo-visibility + run: | + visibility=$(gh api "repos/${{ github.repository }}" --jq '.visibility') + echo "is_public=$( [ "$visibility" = "public" ] && echo true || echo false )" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.github-token }} + + - name: Generate artifact attestation + if: ${{ inputs.create-attestation && steps.repo-visibility.outputs.is_public == 'true' }} + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.IMAGE_REGISTRY }}/${{ inputs.image-name }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true + github-token: ${{ secrets.github-token }} + + - name: Skip attestation notice + if: ${{ inputs.create-attestation && steps.repo-visibility.outputs.is_public != 'true' }} + run: | + echo "::warning::Artifact attestation skipped — not available for private user-owned repositories. Make this repository public to enable attestation." + + release_discussion: + needs: create_release + if: ${{ inputs.create-discussion && inputs.publish && needs.create_release.outputs.full-tag != '' }} + runs-on: ubuntu-latest + permissions: + contents: read # Required by harden-runner + discussions: write # Create announcement discussions + env: + DISCUSSION_REPOSITORY_ID: ${{ secrets.discussion-repository-id }} + DISCUSSION_CATEGORY_ID: ${{ secrets.discussion-category-id }} + steps: + - name: Validate discussion prerequisites + id: check-inputs + env: + GH_TOKEN: ${{ secrets.github-token }} + run: | + if [ -z "${DISCUSSION_REPOSITORY_ID}" ] || [ -z "${DISCUSSION_CATEGORY_ID}" ]; then + echo "::notice::discussion-repository-id and/or discussion-category-id secrets are not set, skipping discussion creation" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + has_discussions=$(gh api graphql -f query=" + query(\$id: ID!) { + node(id: \$id) { + ... on Repository { + hasDiscussionsEnabled + } + } + } + " -f id="${DISCUSSION_REPOSITORY_ID}" --jq '.data.node.hasDiscussionsEnabled') + + if [ "$has_discussions" != "true" ]; then + echo "::notice::Discussions are not enabled on the target repository, skipping discussion creation" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Harden the runner (Audit all outbound calls) + if: ${{ steps.check-inputs.outputs.skip == 'false' }} + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Create an Announcement Discussion for Release + if: ${{ steps.check-inputs.outputs.skip == 'false' }} + uses: abirismyname/create-discussion@c2b7c825241769dda523865ae444a879f6bbd0e0 + with: + title: ${{ needs.create_release.outputs.full-tag }} + body: ${{ needs.create_release.outputs.body }} + repository-id: ${{ env.DISCUSSION_REPOSITORY_ID }} + category-id: ${{ env.DISCUSSION_CATEGORY_ID }} + github-token: ${{ secrets.github-token }} + + publish_release: + needs: [create_release, release_goreleaser, release_image, release_discussion] + if: > + always() && + inputs.publish && + needs.create_release.result == 'success' && + (needs.release_goreleaser.result == 'success' || needs.release_goreleaser.result == 'skipped') && + (needs.release_image.result == 'success' || needs.release_image.result == 'skipped') && + (needs.release_discussion.result == 'success' || needs.release_discussion.result == 'skipped') + runs-on: ubuntu-latest + permissions: + contents: write # Publish draft release + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit - - name: Force update major tag + - name: Publish draft release run: | - git tag -f "${SHORT}" "${FULL}" - git push -f origin "${SHORT}" + gh api \ + --method PATCH \ + "/repos/${{ github.repository }}/releases/${{ needs.create_release.outputs.release-id }}" \ + -F draft=false env: - SHORT: ${{ needs.create_release.outputs.short-tag }} - FULL: ${{ needs.create_release.outputs.full-tag }} + GH_TOKEN: ${{ secrets.github-token }} diff --git a/.github/workflows/test-release.yaml b/.github/workflows/test-release.yaml index 31587e1..a702c34 100644 --- a/.github/workflows/test-release.yaml +++ b/.github/workflows/test-release.yaml @@ -9,42 +9,20 @@ on: jobs: release: permissions: - contents: write - pull-requests: read + contents: write # Create releases and push major version tag + pull-requests: read # Read PR labels for release-drafter + packages: write # Push container images + id-token: write # Federate via Workload Identity for attestation + attestations: write # Create build provenance attestation + discussions: write # Create announcement discussions uses: ./.github/workflows/release.yaml with: publish: true release-config-name: release-drafter.yaml - secrets: - github-token: ${{ secrets.GITHUB_TOKEN }} - release_image: - needs: release - permissions: - contents: read - packages: write - id-token: write - attestations: write - uses: ./.github/workflows/release-image.yaml - with: image-name: ${{ github.repository }} - full-tag: ${{ needs.release.outputs.full-tag }} - short-tag: ${{ needs.release.outputs.short-tag }} create-attestation: true secrets: github-token: ${{ secrets.GITHUB_TOKEN }} - image-registry: ghcr.io - image-registry-username: ${{ github.actor }} image-registry-password: ${{ secrets.GITHUB_TOKEN }} - release_discussion: - needs: release - permissions: - contents: read - discussions: write - uses: ./.github/workflows/release-discussion.yaml - with: - full-tag: ${{ needs.release.outputs.full-tag }} - body: ${{ needs.release.outputs.body }} - secrets: - github-token: ${{ secrets.GITHUB_TOKEN }} - discussion-repository-id: ${{ secrets.DISCUSSION_REPOSITORY_ID }} discussion-category-id: ${{ secrets.DISCUSSION_CATEGORY_ID }} + discussion-repository-id: ${{ secrets.DISCUSSION_REPOSITORY_ID }} diff --git a/README.md b/README.md index a0b7a34..965c408 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Reusable Workflows > [!IMPORTANT] -> This repository has been transferred from `github/ospo-reusable-workflows` to `github-community-projects/ospo-reusable-workflows`. Please update any references to point to the new location. +> The Release Image and Release Discussion workflows are now deprecated. The have been consolidated into the Release workflow. This is a placeholder repo for multiple GitHub Actions we use in open source projects. diff --git a/docs/release-discussion.md b/docs/release-discussion.md index 00885d9..d8db327 100644 --- a/docs/release-discussion.md +++ b/docs/release-discussion.md @@ -1,36 +1,49 @@ # Release Discussion Reusable Workflow -## Inputs +> [!CAUTION] +> This workflow has been deprecated and consolidated into the [Release workflow](release.md). Calling `release-discussion.yaml` directly will fail with an error. Migrate by setting the `discussion-category-id` and `discussion-repository-id` secrets on `release.yaml` instead. + +## Migration + +Replace your existing `release-discussion.yaml` call: ```yaml -- uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@main - permissions: - contents: read - discussions: write +# Before (deprecated) +release_discussion: + needs: release + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release-discussion.yaml@main with: - # Full tag of the image, usually the version (v1.0.0) - full-tag: v1.0.0 - # The body of the release, to be used in the GitHub release UI - body: | - This is a release of the ${{ github.repository }} image. - The full tag is ${{ inputs.full-tag }}. - The short tag is ${{ inputs.short-tag }}. + full-tag: ${{ needs.release.outputs.full-tag }} + body: ${{ needs.release.outputs.body }} secrets: - # The GitHub token to use github-token: ${{ secrets.GITHUB_TOKEN }} - # Discussion Repository ID discussion-repository-id: ${{ secrets.DISCUSSION_REPOSITORY_ID }} - # Discussion Category ID discussion-category-id: ${{ secrets.DISCUSSION_CATEGORY_ID }} ``` -## Outputs +With the consolidated `release.yaml` secrets: + +```yaml +# After +release: + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release.yaml@main + with: + release-config-name: release-drafter.yaml + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + discussion-category-id: ${{ secrets.DISCUSSION_CATEGORY_ID }} + discussion-repository-id: ${{ secrets.DISCUSSION_REPOSITORY_ID }} +``` + +Key changes: +- `full-tag` and `body` no longer need to be passed — they are handled internally +- Discussion IDs moved from required secrets to optional secrets -None +See the full [Release workflow documentation](release.md) for all available inputs. ## Notes -In order to get the discussion repository ID and category ID, you can use the GitHub GraphQL API Explorer: https://docs.github.com/en/graphql/overview/explorer with the following query (replace `OWNER` and `REPO` with the appropriate values): +To get the discussion repository ID and category ID, use the GitHub GraphQL API Explorer: https://docs.github.com/en/graphql/overview/explorer with the following query (replace `OWNER` and `REPO` with the appropriate values): ```graphql query { diff --git a/docs/release-image.md b/docs/release-image.md index 69a2d0d..4598169 100644 --- a/docs/release-image.md +++ b/docs/release-image.md @@ -1,34 +1,48 @@ # Release Image Reusable Workflow -## Inputs +> [!CAUTION] +> This workflow has been deprecated and consolidated into the [Release workflow](release.md). Calling `release-image.yaml` directly will fail with an error. Migrate by setting the `image-name` input on `release.yaml` instead. + +## Migration + +Replace your existing `release-image.yaml` call: + +```yaml +# Before (deprecated) +release_image: + needs: release + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release-image.yaml@main + with: + image-name: ${{ github.repository }} + full-tag: ${{ needs.release.outputs.full-tag }} + short-tag: ${{ needs.release.outputs.short-tag }} + create-attestation: true + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + image-registry: ghcr.io + image-registry-username: ${{ github.actor }} + image-registry-password: ${{ secrets.GITHUB_TOKEN }} +``` + +With the consolidated `release.yaml` inputs: ```yaml -- uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release-image.yaml@main - permissions: - contents: read - packages: write - id-token: write - attestations: write +# After +release: + uses: github-community-projects/ospo-reusable-workflows/.github/workflows/release.yaml@main with: - # Image name, usually owner/repository (github-community-projects/ospo-reusable-workflows) + release-config-name: release-drafter.yaml image-name: ${{ github.repository }} - # Full tag of the image, usually the version (v1.0.0) - full-tag: v1.0.0 - # Short tag of the image, usually the major version (v1) - short-tag: v1 - # Flag to create an attestation create-attestation: true secrets: - # The GitHub token to use github-token: ${{ secrets.GITHUB_TOKEN }} - # Image repository url - image-registry: ${{ secrets.IMAGE_REPOSITORY_URL }} - # Image repository username - image-registry-username: ${{ secrets.IMAGE_REPOSITORY_USERNAME }} - # Image repository password - image-registry-password: ${{ secrets.IMAGE_REPOSITORY_PASSWORD }} + image-registry-password: ${{ secrets.GITHUB_TOKEN }} ``` -## Outputs +Key changes: +- `full-tag` and `short-tag` no longer need to be passed — they are handled internally +- `image-registry` defaults to `ghcr.io` +- `image-registry-username` defaults to `github.actor` +- Registry credentials moved from required secrets to optional (with defaults) -None +See the full [Release workflow documentation](release.md) for all available inputs. diff --git a/docs/release.md b/docs/release.md index 4415c58..88d3977 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,5 +1,7 @@ # Release Reusable Workflow +Consolidated release workflow that creates a draft release, optionally builds artifacts (GoReleaser, Docker images), creates GitHub Discussions announcements, and publishes the release after all jobs succeed. This draft-first pattern supports repositories with immutable releases enabled. + ## Inputs ```yaml @@ -7,17 +9,53 @@ permissions: contents: write pull-requests: read + packages: write + id-token: write + attestations: write + discussions: write with: - # Boolean flag whether to publish the release, default is true + # Publish the release after all jobs complete. When false, the release + # remains a draft for manual review. Default is true. publish: true - # The name of the configuration file to use, default is release-drafter.yml + # The name of the configuration file to use # from the release-drafter/release-drafter GitHub Action release-config-name: release-drafter.yml - # Boolean flag whether to update major tag to latest full semver tag, default is true - update-major-tag: true + + # --- Optional: GoReleaser build/upload --- + # Setting goreleaser-config-path enables the GoReleaser job + # Path to GoReleaser config file (e.g., .goreleaser.yaml) + goreleaser-config-path: .goreleaser.yaml + # Path to go.mod or go.work file for Go version detection, default is go.mod + go-version-file: go.mod + + # --- Optional: Docker image build/push --- + # Setting image-name enables the image build/push job + # Image name, usually owner/repository + image-name: ${{ github.repository }} + # Container registry URL, default is ghcr.io + image-registry: ghcr.io + # Container registry username, default is github.actor + image-registry-username: ${{ github.actor }} + # Comma-separated list of target platforms, default is linux/amd64,linux/arm64 + image-platforms: linux/amd64,linux/arm64 + # Flag to create build provenance attestations, default is false + # Attestation is only available for public repositories. Private repos + # will see a warning and skip attestation automatically. + create-attestation: true + create-discussion: true + secrets: - # The GitHub token to use + # The GitHub token to use (required) github-token: ${{ secrets.GITHUB_TOKEN }} + # Container registry password (required when image-name is set) + image-registry-password: ${{ secrets.GITHUB_TOKEN }} + + # --- Optional: GitHub Discussion announcement --- + # Setting both discussion IDs enables the discussion creation job + # GraphQL ID of the discussion category + discussion-category-id: ${{ secrets.DISCUSSION_CATEGORY_ID }} + # GraphQL ID of the repository for discussions + discussion-repository-id: ${{ secrets.DISCUSSION_REPOSITORY_ID }} ``` ## Outputs @@ -36,3 +74,48 @@ jobs: SHORT_TAG: ${{ needs.release.outputs.short-tag }} BODY: ${{ needs.release.outputs.body }} ``` + +## Jobs + +The workflow runs up to six jobs: + +1. **create_release** - Always runs. Creates a draft release via release-drafter, then creates and pushes the full and major version git tags. +2. **release_goreleaser** - Runs when `goreleaser-config-path` is set. Builds Go binaries, uploads artifacts to the draft release, and optionally creates attestations. +3. **release_image** - Runs when `image-name` is set. Builds and pushes a multi-platform Docker image, and optionally creates attestations. +4. **release_discussion** - Runs when `create-discussion` is set. Both `discussion-category-id` and `discussion-repository-id` secrets are required if so. Creates a GitHub Discussions announcement. +5. **publish_release** - Runs when `publish` is true and all preceding jobs succeed (or are skipped). Publishes the draft release. + +## GoReleaser Configuration + +When using the `goreleaser-config-path` input, your GoReleaser config **must** disable release and changelog management since this workflow handles both via release-drafter: + +```yaml +release: + disable: true + +changelog: + disable: true +``` + +Without these settings, GoReleaser will attempt to create its own GitHub release, conflicting with the draft release created by release-drafter. + +## Notes + +- The draft-first pattern supports repositories with **immutable releases** enabled. The release is created as a draft, artifacts are uploaded, and only then is it published. +- Artifact attestation requires a **public repository**. Private user-owned or organization repositories on free plans will see a warning and skip attestation automatically. +- To get the discussion repository ID and category ID, use the GitHub GraphQL API Explorer: https://docs.github.com/en/graphql/overview/explorer with the following query (replace `OWNER` and `REPO` with the appropriate values): + +```graphql +query { + repository(owner: "OWNER", name: "REPO") { + id + discussionCategories(first: 50) { + nodes { + id + name + slug + } + } + } +} +```