diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06a867946c..709bdd48ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,15 @@ on: tag: required: false type: string + default: '' + dry_run: + required: false + type: boolean + default: false + runner: + required: false + type: string + default: 'ubuntu-24.04' defaults: run: @@ -27,7 +36,7 @@ jobs: security-events: write # for github/codeql-action/upload-sarif to upload SARIF results packages: write # for docker/build-push-action to push to GHCR id-token: write # for docker/login to login to NGINX registry - runs-on: ${{ github.event_name != 'pull_request' && contains(inputs.image, 'plus') && 'kic-plus' || 'ubuntu-24.04' }} + runs-on: ${{ inputs.runner }} services: registry: image: registry:3 @@ -37,7 +46,7 @@ jobs: - name: Checkout Repository uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - ref: ${{ inputs.tag != '' && format('refs/tags/v{0}', inputs.tag) || github.ref }} + ref: ${{ (inputs.tag != '' && !inputs.dry_run ) && format('refs/tags/v{0}', inputs.tag) || github.ref }} - name: Fetch Cached Artifacts uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 @@ -115,7 +124,7 @@ jobs: type=edge type=schedule type=ref,event=pr - type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') && inputs.tag == '' }} type=raw,value=${{ inputs.tag }},enable=${{ inputs.tag != '' }} labels: | org.opencontainers.image.documentation=https://docs.nginx.com/nginx-gateway-fabric @@ -140,7 +149,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.annotations }} - push: true + push: ${{ !inputs.dry_run }} platforms: ${{ inputs.platforms }} cache-from: type=gha,scope=${{ inputs.image }} cache-to: type=gha,scope=${{ inputs.image }},mode=max @@ -157,12 +166,14 @@ jobs: ${{ contains(inputs.image, 'plus') && format('"nginx-repo.key={0}"', secrets.NGINX_KEY) || '' }} - name: Inspect SBOM and output manifest + if: ${{ !inputs.dry_run }} run: | docker buildx imagetools inspect localhost:5000/nginx-gateway-fabric/${{ inputs.image }}:${{ steps.meta.outputs.version }} --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sbom-${{ inputs.image }}.json docker buildx imagetools inspect localhost:5000/nginx-gateway-fabric/${{ inputs.image }}:${{ steps.meta.outputs.version }} --raw - name: Scan SBOM id: scan + if: ${{ !inputs.dry_run }} uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6.5.1 with: sbom: "sbom-${{ inputs.image }}.json" @@ -172,8 +183,8 @@ jobs: - name: Upload scan result to GitHub Security tab uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 + if: ${{ !inputs.dry_run }} continue-on-error: true with: sarif_file: ${{ steps.scan.outputs.sarif }} category: build-${{ inputs.image }} - if: always() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 729e9cb93e..51ee235425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,18 +5,33 @@ on: branches: - main - release-* - tags: - - "v[0-9]+.[0-9]+.[0-9]+*" pull_request: branches: - "**" schedule: - cron: "0 4 * * *" # run every day at 4am UTC + workflow_call: + inputs: + is_production_release: + required: false + type: boolean + default: false + release_version: + required: false + type: string + default: '' + dry_run: + required: false + type: boolean + default: false defaults: run: shell: bash +env: + GOPROXY: ${{ (github.repository_owner == 'nginx' && (inputs.is_production_release || github.event_name == 'push' && github.ref == 'refs/heads/main') && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_ENDPOINT)) || (github.repository_owner == 'nginx' && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_DEV_ENDPOINT) || 'direct') }} + concurrency: group: ${{ github.ref_name }}-ci cancel-in-progress: true @@ -127,7 +142,7 @@ jobs: binary: name: Build Binary - runs-on: ubuntu-24.04 + runs-on: ${{ github.repository_owner == 'nginx' && (inputs.is_production_release || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'ubuntu-24.04-amd64' || 'ubuntu-24.04' }} needs: [vars, unit-tests, njs-unit-tests] permissions: contents: write # for goreleaser/goreleaser-action and lucacome/draft-release to create/update releases @@ -147,12 +162,18 @@ jobs: go.sum .github/.cache/buster-for-binary + - name: Set Go module cache + run: | + mkdir -p ${{ github.workspace }}/.gocache + echo "GOMODCACHE=${{ github.workspace }}/.gocache" >> $GITHUB_ENV + echo "GOCACHE=${{ github.workspace }}/.gocache" >> $GITHUB_ENV + - name: Create/Update Draft uses: lucacome/draft-release@00f74370c044c322da6cb52acc707d62c7762c71 # v1.2.4 with: minor-label: "enhancement" major-label: "change" - publish: ${{ github.ref_type == 'tag' }} + publish: ${{ inputs.is_production_release && (inputs.dry_run == false || inputs.dry_run == null) }} collapse-after: 20 notes-header: | *Below is the auto-generated changelog, which includes all PRs that went into the release. @@ -160,18 +181,18 @@ jobs: if: ${{ github.event_name == 'push' && github.ref != 'refs/heads/main' }} - name: Download Syft + if: ${{ inputs.is_production_release }} uses: anchore/sbom-action/download-syft@da167eac915b4e86f08b264dbdbc867b61be6f0c # v0.20.5 - if: github.ref_type == 'tag' - name: Install Cosign + if: ${{ inputs.is_production_release }} uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - if: github.ref_type == 'tag' - name: Build binary uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: version: v2.12.0 # renovate: datasource=github-tags depName=goreleaser/goreleaser - args: ${{ github.ref_type == 'tag' && 'release' || 'build --snapshot' }} --clean + args: ${{ (inputs.is_production_release && (inputs.dry_run == false || inputs.dry_run == null)) && 'release' || 'build --snapshot' }} --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GOPATH: ${{ needs.vars.outputs.go_path }} @@ -200,6 +221,9 @@ jobs: with: image: ${{ matrix.image }} platforms: ${{ matrix.platforms }} + tag: ${{ inputs.release_version || '' }} + dry_run: ${{ inputs.dry_run || false}} + runner: ${{ github.repository_owner == 'nginx' && (inputs.is_production_release || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'ubuntu-24.04-amd64' || 'ubuntu-24.04' }} permissions: contents: read # for docker/build-push-action to read repo content security-events: write # for github/codeql-action/upload-sarif to upload SARIF results @@ -214,6 +238,9 @@ jobs: with: image: plus platforms: "linux/arm64, linux/amd64" + tag: ${{ inputs.release_version || '' }} + dry_run: ${{ inputs.dry_run || false }} + runner: ${{ github.repository_owner == 'nginx' && (inputs.is_production_release || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'ubuntu-24.04-amd64' || 'ubuntu-24.04' }} permissions: contents: read # for docker/build-push-action to read repo content security-events: write # for github/codeql-action/upload-sarif to upload SARIF results @@ -259,6 +286,8 @@ jobs: image: ${{ matrix.image }} k8s-version: ${{ matrix.k8s-version }} enable-experimental: ${{ matrix.enable-experimental }} + production-release: ${{ inputs.is_production_release == true && (inputs.dry_run == false || inputs.dry_run == null) }} + release_version: ${{ inputs.release_version }} secrets: inherit permissions: contents: write @@ -284,9 +313,9 @@ jobs: publish-helm: name: Package and Publish Helm Chart - runs-on: ubuntu-24.04 + runs-on: ${{ github.repository_owner == 'nginx' && (inputs.is_production_release || (github.event_name == 'push' && github.ref == 'refs/heads/main')) && 'ubuntu-24.04-amd64' || 'ubuntu-24.04' }} needs: [vars, helm-tests] - if: ${{ github.event_name == 'push' && ! startsWith(github.ref, 'refs/heads/release-') }} + if: ${{ (inputs.is_production_release && (inputs.dry_run == false || inputs.dry_run == null)) || (github.event_name == 'push' && ! startsWith(github.ref, 'refs/heads/release-')) }} permissions: contents: read packages: write # for helm to push to GHCR @@ -304,10 +333,11 @@ jobs: - name: Package id: package run: | - output=$(helm package ${{ github.ref_type != 'tag' && '--app-version edge --version 0.0.0-edge' || '' }} charts/nginx-gateway-fabric) + output=$(helm package ${{ !inputs.is_production_release && '--app-version edge --version 0.0.0-edge' || '' }} charts/nginx-gateway-fabric) echo "path=$(basename -- $(echo $output | cut -d: -f2))" >> $GITHUB_OUTPUT - name: Push to GitHub Container Registry + if: ${{ inputs.dry_run == false || inputs.dry_run == null }} run: | helm push ${{ steps.package.outputs.path }} oci://ghcr.io/nginx/charts diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index f63908ee28..1a74014a77 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -12,6 +12,14 @@ on: enable-experimental: required: true type: boolean + production-release: + required: false + type: boolean + default: false + release_version: + required: false + type: string + default: '' defaults: run: @@ -20,6 +28,7 @@ defaults: env: PLUS_USAGE_ENDPOINT: ${{ secrets.JWT_PLUS_REPORTING_ENDPOINT }} ENABLE_EXPERIMENTAL: ${{ inputs.enable-experimental }} + GOPROXY: ${{ github.repository_owner == 'nginx' && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_DEV_ENDPOINT) || 'direct' }} permissions: contents: read @@ -61,7 +70,8 @@ jobs: type=edge type=schedule type=ref,event=pr - type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') && !inputs.production-release }} + type=raw,value={{inputs.release_version}},enable=${{ inputs.production-release && inputs.release_version != '' }} - name: NGINX Docker meta id: nginx-meta @@ -74,7 +84,8 @@ jobs: type=edge type=schedule type=ref,event=pr - type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} + type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') && !inputs.production-release }} + type=raw,value={{inputs.release_version}},enable=${{ inputs.production-release && inputs.release_version != '' }} - name: Build binary uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 @@ -162,7 +173,7 @@ jobs: path: ./tests/conformance-profile.yaml - name: Upload profile to release - if: ${{ startsWith(github.ref, 'refs/tags/') && inputs.enable-experimental }} + if: ${{ inputs.production-release && inputs.enable-experimental }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release upload ${{ github.ref_name }} conformance-profile.yaml --clobber diff --git a/.github/workflows/functional.yml b/.github/workflows/functional.yml index 587961eb35..c9f2d80c8e 100644 --- a/.github/workflows/functional.yml +++ b/.github/workflows/functional.yml @@ -16,6 +16,7 @@ defaults: env: PLUS_USAGE_ENDPOINT: ${{ secrets.JWT_PLUS_REPORTING_ENDPOINT }} + GOPROXY: ${{ github.repository_owner == 'nginx' && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_DEV_ENDPOINT) || 'direct' }} permissions: contents: read diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index e092066b90..84d1d628ba 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -44,7 +44,6 @@ jobs: type=edge type=schedule type=ref,event=pr - type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} - name: NGINX Docker meta id: nginx-meta @@ -57,7 +56,6 @@ jobs: type=edge type=schedule type=ref,event=pr - type=ref,event=branch,suffix=-rc,enable=${{ startsWith(github.ref, 'refs/heads/release') }} - name: Build NGF Docker Image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f2ecdd9100..074b179f60 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,6 +12,9 @@ defaults: run: shell: bash +env: + GOPROXY: ${{ github.repository_owner == 'nginx' && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_DEV_ENDPOINT) || 'direct' }} + concurrency: group: ${{ github.ref_name }}-lint cancel-in-progress: true diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml new file mode 100644 index 0000000000..3351269e59 --- /dev/null +++ b/.github/workflows/production-release.yml @@ -0,0 +1,85 @@ +name: Production Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Release version (e.g., v2.0.3)' + required: true + type: string + dry_run: + description: 'If true, does a dry run of the production workflow' + required: false + type: boolean + +run-name: ${{ inputs.dry_run && '[DRY RUN] ' || '' }}Release ${{ inputs.version }} by @${{ github.actor }} + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + create-tag-and-release: + runs-on: ubuntu-24.04 + if: startsWith(github.ref, 'refs/heads/release-') + permissions: + contents: write + steps: + - name: Validate Release Branch and Version + run: | + echo "Validating release from: ${GITHUB_REF}" + + INPUT_VERSION="${{ github.event.inputs.version }}" + + # Validate version format + if [[ ! "${INPUT_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "❌ Invalid version format: ${INPUT_VERSION}" + echo "Expected format: v1.2.3" + exit 1 + fi + + echo "✅ Valid release branch: ${GITHUB_REF}" + echo "✅ Valid version format: ${INPUT_VERSION}" + + - name: Checkout Repository + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 + with: + fetch-depth: 0 + + - name: Create Release Tag + run: | + VERSION="${{ github.event.inputs.version }}" + git config user.name "NGF Release Bot" + git config user.email "integrations@nginx.com" + + if git rev-parse --verify "refs/tags/${VERSION}" >/dev/null 2>&1; then + echo "Tag ${VERSION} already exists - skipping tag creation" + else + echo "Creating annotated tag ${VERSION}" + git tag -a "${VERSION}" -m "Release ${VERSION}" + + if [[ "${{ inputs.dry_run }}" == "true" ]]; then + echo "DRY RUN: Would push tag ${VERSION}" + git push --dry-run origin "${VERSION}" + else + git push origin "${VERSION}" + echo "Created and pushed tag: ${VERSION}" + fi + fi + + production-build: + needs: create-tag-and-release + uses: ./.github/workflows/ci.yml + with: + is_production_release: true + release_version: ${{ github.event.inputs.version }} + dry_run: ${{ github.event.inputs.dry_run }} + secrets: inherit + permissions: + contents: write + packages: write + id-token: write + security-events: write diff --git a/.github/workflows/renovate-build.yml b/.github/workflows/renovate-build.yml index 5d4ab349e9..8b384c388e 100644 --- a/.github/workflows/renovate-build.yml +++ b/.github/workflows/renovate-build.yml @@ -11,6 +11,9 @@ defaults: run: shell: bash +env: + GOPROXY: ${{ github.repository_owner == 'nginx' && format('https://{0}:{1}@{2}', secrets.ARTIFACTORY_USER, secrets.ARTIFACTORY_TOKEN, secrets.ARTIFACTORY_DEV_ENDPOINT) || 'direct' }} + concurrency: group: ${{ github.ref_name }}-renovate cancel-in-progress: true diff --git a/docs/developer/release-process.md b/docs/developer/release-process.md index 1297d0b83b..7cd056c9a3 100644 --- a/docs/developer/release-process.md +++ b/docs/developer/release-process.md @@ -40,6 +40,7 @@ To create a new release, follow these steps: format `Release X.Y.Z`. 2. Stop merging any new work into the main branch. 3. Create a release branch following the `release-X.Y` naming convention. + - Once the release branch is created, reach out to the infra team to get it added to the runner permissions. 4. Once the release branch pipeline completes, run tests using the `release-X.X-rc` images that are pushed to Github (for example, `release-1.3-rc`). 1. Kick off the [longevity tests](https://github.com/nginx/nginx-gateway-fabric/blob/main/tests/README.md#longevity-testing) for both OSS and Plus. You'll need to create two clusters and VMs for this. Before running, update your `vars.env` file with the proper image tag and prefixes. NGF and nginx images will be available from `ghcr.io`, and nginx plus will be available in GCP (`us-docker.pkg.dev//nginx-gateway-fabric/nginx-plus`). These tests need to run for 4 days before releasing. The results should be committed to the main branch and then cherry-picked to the release branch. 2. Kick off the [NFR workflow](https://github.com/nginx/nginx-gateway-fabric/actions/workflows/nfr.yml) in the browser. For `image_tag`, use `release-X.X-rc`, and for `version`, use the upcoming `X.Y.Z` NGF version. Run the workflow on the new release branch. This will run all of the NFR tests which are automated and open a PR with the results files when it is complete. Review this PR and make any necessary changes before merging. Once merged, be sure to cherry-pick the commit to the main branch as well (the original PR targets the release branch). @@ -55,14 +56,9 @@ To create a new release, follow these steps: created. If included, use the Release Notes specified in a PR. - If the supported Gateway API minor version has changed since the last release, add a note to the release notes explaining if the previous version is no longer supported. - Merge the release PR once it has received all necessary approvals. -6. Ensure you are on the latest version of the release branch and are up-to-date on all commits, then create and push the release tag in the format `vX.Y.Z`. - - ```shell - git tag vX.Y.Z - git push upstream vX.Y.Z - ``` - +6. Once you are ready to release, run the [Production Release](https://github.com/nginx/nginx-gateway-fabric/actions/workflows/production-release.yml) workflow with the correct tag e.g. `v2.1.0`. (Note: It is also possible to do a dry run of the production release workflow for verification if required. This will not push the tag, images, and chart, and won't publish the release) As a result, the CI/CD pipeline will: + - Create and push the tag - Build NGF, NGINX and NGINX Plus container images with the release tag `X.Y.Z` and push them to the registries. - Package and publish the Helm chart to the registry. - Create a GitHub release with an autogenerated changelog and attached release artifacts.