Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
test-framework: [net8.0, net9.0]
runs-on: ${{ matrix.os }}
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/mutation-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:

steps:
- name: Check out repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Set up .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
with:
dotnet-version: |
8.0.x
Expand Down
178 changes: 137 additions & 41 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,59 @@ on:
type: boolean

permissions:
contents: write
id-token: write
attestations: write
contents: read

concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref }}
cancel-in-progress: false

jobs:
preflight:
name: Validate release tag
if: github.repository == 'Widthdom/CodeIndex'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
ref: ${{ steps.release.outputs.ref }}
steps:
- name: Validate release tag
id: release
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_TAG_NAME: ${{ inputs.tag_name }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
tag="$INPUT_TAG_NAME"
else
if [ "$REF_TYPE" != "tag" ]; then
echo "Release workflow must run from a tag ref, got ${REF_TYPE}:${REF_NAME}." >&2
exit 1
fi
tag="$REF_NAME"
fi

if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a v-prefixed SemVer version, got: ${tag}" >&2
exit 1
fi

echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
echo "ref=refs/tags/${tag}" >> "$GITHUB_OUTPUT"

release:
if: github.repository == 'Widthdom/CodeIndex'
needs: preflight
timeout-minutes: 90
permissions:
contents: read
strategy:
matrix:
# Keep this matrix in sync with docs/platform-support.md and install.sh
Expand Down Expand Up @@ -52,7 +98,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ inputs.tag_name || github.ref }}
ref: ${{ needs.preflight.outputs.ref }}

# The release workflow runs the same Windows test suite, so it needs the
# same temp-root pinning as .github/workflows/dotnet.yml: even after the
Expand Down Expand Up @@ -474,13 +520,18 @@ jobs:
create-release:
if: github.repository == 'Widthdom/CodeIndex'
runs-on: ubuntu-latest
needs: release
needs: [preflight, release]
timeout-minutes: 45
permissions:
contents: write
id-token: write
attestations: write
environment: release-production
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.tag_name || github.ref }}
ref: ${{ needs.preflight.outputs.ref }}

- name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
Expand Down Expand Up @@ -554,7 +605,7 @@ jobs:
test -s release-files/sha256sums.txt.asc

- name: Attest release artifacts
uses: actions/attest-build-provenance@v2
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
with:
subject-path: |
release-files/*.tar.gz
Expand Down Expand Up @@ -588,7 +639,7 @@ jobs:
if: ${{ !inputs.allow_generated_notes }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ inputs.tag_name || github.ref_name }}
TAG_NAME: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
version="${TAG_NAME#v}"
Expand Down Expand Up @@ -616,7 +667,7 @@ jobs:
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ inputs.tag_name || github.ref_name }}
TAG_NAME: ${{ needs.preflight.outputs.tag }}
USE_GENERATED_NOTES: ${{ inputs.allow_generated_notes || false }}
run: |
set -euo pipefail
Expand Down Expand Up @@ -652,7 +703,7 @@ jobs:
- name: Wait for release assets to be downloadable
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ inputs.tag_name || github.ref_name }}
TAG_NAME: ${{ needs.preflight.outputs.tag }}
# install.sh downloads BOTH the linux-x64 tarball and sha256sums.txt,
# and the CDN can propagate each release asset independently. Polling
# only the tarball let the verify step run while sha256sums.txt was
Expand Down Expand Up @@ -734,7 +785,7 @@ jobs:
deadline=$((SECONDS + wait_seconds))
attempt=1
while :; do
code="$(curl -fsSL --range 0-0 -o /dev/null -w '%{http_code}' "$url" || true)"
code="$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 5 --retry-delay 5 --retry-all-errors --range 0-0 -o /dev/null -w '%{http_code}' "$url" || true)"
if [ "$code" = "200" ] || [ "$code" = "206" ]; then
echo "Asset ${asset} reachable after $attempt attempt(s)."
reachable=1
Expand All @@ -757,7 +808,7 @@ jobs:

- name: Verify install.sh against the published release
env:
TAG_NAME: ${{ inputs.tag_name || github.ref_name }}
TAG_NAME: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
export CDIDX_INSTALL_DIR="$(mktemp -d)/bin"
Expand Down Expand Up @@ -820,13 +871,18 @@ jobs:
publish-nuget:
if: github.repository == 'Widthdom/CodeIndex'
runs-on: ubuntu-latest
needs: release
needs: [preflight, release]
timeout-minutes: 30
permissions:
contents: read
id-token: write
attestations: write
environment: nuget-production
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.tag_name || github.ref }}
ref: ${{ needs.preflight.outputs.ref }}

- name: Set up .NET
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
Expand All @@ -837,7 +893,7 @@ jobs:
id: version
run: |
set -euo pipefail
TAG="${{ inputs.tag_name || github.ref_name }}"
TAG="${{ needs.preflight.outputs.tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a v-prefixed SemVer version, got: ${TAG}" >&2
exit 1
Expand All @@ -859,15 +915,21 @@ jobs:
run: |
set -euo pipefail
package_url="https://api.nuget.org/v3-flatcontainer/cdidx/${VERSION}/cdidx.${VERSION}.nupkg"
status="$(curl -sS -o /tmp/cdidx-nuget-head -w '%{http_code}' -I "$package_url")"
response_headers="$(mktemp "${RUNNER_TEMP:-/tmp}/cdidx-nuget-head.XXXXXX")"
cleanup() {
rm -f "$response_headers"
}
trap cleanup EXIT

status="$(curl -sS --connect-timeout 10 --max-time 30 --retry 3 --retry-delay 2 --retry-all-errors -o "$response_headers" -w '%{http_code}' -I "$package_url")"
if [ "$status" = "200" ]; then
echo "NuGet package cdidx ${VERSION} is already published; refusing to mask this as a duplicate." >&2
exit 1
fi

if [ "$status" != "404" ]; then
echo "Could not verify NuGet package availability for cdidx ${VERSION}; HTTP ${status}." >&2
cat /tmp/cdidx-nuget-head >&2 || true
cat "$response_headers" >&2 || true
exit 1
fi

Expand Down Expand Up @@ -936,21 +998,35 @@ jobs:
exit 1
fi

- name: Attest NuGet package artifacts
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
with:
subject-path: |
nupkg/*.nupkg
nupkg/*.snupkg

- name: NuGet trusted publishing login
id: nuget-login
uses: NuGet/login@ebc737b6fc418a6ca0073cf116ec8dc156d8b81e # v1
with:
user: Widthdom

- name: Publish to NuGet
run: |
set -euo pipefail
dotnet nuget push nupkg/*.nupkg \
--api-key ${{ secrets.NUGET_API_KEY }} \
--source https://api.nuget.org/v3/index.json \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--source https://www.nuget.org/api/v2/package \
--no-symbols
dotnet nuget push nupkg/*.snupkg \
--api-key ${{ secrets.NUGET_API_KEY }} \
--source https://api.nuget.org/v3/index.json
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--source https://www.nuget.org/api/v2/package

publish-container:
if: github.repository == 'Widthdom/CodeIndex'
runs-on: ubuntu-latest
needs: create-release
needs: [preflight, create-release]
timeout-minutes: 90
permissions:
contents: read
id-token: write
Expand All @@ -960,13 +1036,13 @@ jobs:
- name: Checkout release tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.tag_name || github.ref }}
ref: ${{ needs.preflight.outputs.ref }}

- name: Extract version from tag
id: version
run: |
set -euo pipefail
TAG="${{ inputs.tag_name || github.ref_name }}"
TAG="${{ needs.preflight.outputs.tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a v-prefixed SemVer version, got: ${TAG}" >&2
exit 1
Expand All @@ -990,17 +1066,17 @@ jobs:
} >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push container image
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64,linux/arm64
Expand All @@ -1012,20 +1088,23 @@ jobs:
publish-homebrew:
if: github.repository == 'Widthdom/CodeIndex'
runs-on: ubuntu-latest
needs: create-release
needs: [preflight, create-release]
timeout-minutes: 30
permissions:
contents: read
environment: homebrew-production
steps:
- name: Checkout release tag
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ inputs.tag_name || github.ref }}
ref: ${{ needs.preflight.outputs.ref }}

- name: Extract release tag
id: release
run: |
set -euo pipefail
TAG="${{ inputs.tag_name || github.ref_name }}"
TAG="${{ needs.preflight.outputs.tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Release tag must be a v-prefixed SemVer version, got: ${TAG}" >&2
exit 1
Expand All @@ -1034,6 +1113,11 @@ jobs:
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "revision=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Download release artifacts for checksum calculation
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: homebrew-artifacts

- name: Bump Homebrew formula
env:
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
Expand All @@ -1048,20 +1132,32 @@ jobs:
VERSION="${TAG_NAME#v}"
REPO_URL="https://github.com/${GITHUB_REPOSITORY}"
RELEASE_URL="${REPO_URL}/releases/download/${TAG_NAME}"
CHECKSUMS_URL="${RELEASE_URL}/sha256sums.txt"
checksums="$(mktemp)"
curl -fsSL "$CHECKSUMS_URL" -o "$checksums"
artifact_root="homebrew-artifacts"
tap_dir=""
cleanup() {
if [ -n "$tap_dir" ]; then
rm -rf "$tap_dir"
fi
}
trap cleanup EXIT

sha_for() {
awk -v name="$1" '$2 == name { print $1; found=1 } END { exit found ? 0 : 1 }' "$checksums"
sha_for_artifact() {
asset="$1"
path="$(find "$artifact_root" -type f -name "$asset" -print -quit)"
if [ -z "$path" ]; then
echo "Release artifact ${asset} was not found under ${artifact_root}." >&2
exit 1
fi
sha256sum "$path" | awk '{ print $1 }'
}

linux_x64_sha="$(sha_for CodeIndex-linux-x64.tar.gz)"
linux_arm64_sha="$(sha_for CodeIndex-linux-arm64.tar.gz)"
osx_arm64_sha="$(sha_for CodeIndex-osx-arm64.tar.gz)"
linux_x64_sha="$(sha_for_artifact CodeIndex-linux-x64.tar.gz)"
linux_arm64_sha="$(sha_for_artifact CodeIndex-linux-arm64.tar.gz)"
osx_arm64_sha="$(sha_for_artifact CodeIndex-osx-arm64.tar.gz)"

tap_dir="$(mktemp -d)"
git clone "https://x-access-token:${TAP_TOKEN}@github.com/Widthdom/homebrew-tap.git" "$tap_dir"
credential_helper='!f() { echo username=x-access-token; echo "password=${TAP_TOKEN}"; }; f'
git -c credential.helper="$credential_helper" clone "https://github.com/Widthdom/homebrew-tap.git" "$tap_dir"
mkdir -p "$tap_dir/Formula"

cat > "$tap_dir/Formula/codeindex.rb" <<EOF
Expand Down Expand Up @@ -1117,4 +1213,4 @@ jobs:
fi

git -C "$tap_dir" commit -m "Update codeindex to ${TAG_NAME}"
git -C "$tap_dir" push origin HEAD:main
git -C "$tap_dir" -c credential.helper="$credential_helper" push origin HEAD:main
Loading
Loading