Skip to content
Merged
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
113 changes: 113 additions & 0 deletions .github/workflows/release-lsp-binary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
name: Release LSP Binary

on:
push:
tags:
- 'iii-lsp/v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g., iii-lsp/v1.0.0)'
required: true
type: string

permissions:
contents: write

concurrency:
group: release-iii-lsp-${{ github.ref }}
cancel-in-progress: false
Comment on lines +17 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In GitHub Actions, for top-level concurrency.group, is inputsavailable onworkflow_dispatch, and what does github.refresolve to onpushvsworkflow_dispatch?

💡 Result:

  • Is inputs available in a top-level concurrency.group on workflow_dispatch?
    Yes. The workflow-level concurrency key supports expressions using the github, inputs, and vars contexts, and the inputs context is available for workflows triggered by workflow_dispatch. [1]

  • What does github.ref resolve to?

    • On push: github.ref / GITHUB_REF is the updated ref that was pushed (branch or tag), e.g. refs/heads/main or refs/tags/v1.2.3. [2]
    • On workflow_dispatch: github.ref / GITHUB_REF is the branch or tag that received the dispatch (the ref you selected in the UI or passed as ref via the API/CLI). [3]

Sources
[1] GitHub Docs – Context availability table; concurrency supports github, inputs, vars, and inputs is available for workflow_dispatch workflows. (docs.github.com)
[2] GitHub Docs – push event: GITHUB_REF is the “Updated ref”. (docs.github.com)
[3] GitHub Docs – workflow_dispatch event: GITHUB_REF is “Branch or tag that received dispatch”. (docs.github.com)

Citations:


Use the release tag in the concurrency key instead of the workflow ref.

Line 18 keys concurrency by github.ref. On workflow_dispatch, this resolves to the branch from which the dispatch was triggered (e.g., refs/heads/main), not the release tag. The same release tag can therefore run under different concurrency groups when triggered via workflow_dispatch vs. a direct tag push, allowing concurrent execution instead of serialization.

Proposed fix
 concurrency:
-  group: release-iii-lsp-${{ github.ref }}
+  group: release-iii-lsp-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
   cancel-in-progress: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
concurrency:
group: release-iii-lsp-${{ github.ref }}
cancel-in-progress: false
concurrency:
group: release-iii-lsp-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
cancel-in-progress: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-lsp-binary.yml around lines 17 - 19, The
concurrency group currently uses github.ref (group: release-iii-lsp-${{
github.ref }}) which resolves to branches for workflow_dispatch; change it to
key off the release tag instead by using the release tag fallback expression,
e.g. set group to release-iii-lsp-${{ github.event.release.tag_name ||
github.ref_name }}, so the concurrency group is derived from the actual release
tag (reference the concurrency group line and the github.event.release.tag_name
/ github.ref_name symbols).


jobs:
# ──────────────────────────────────────────────────────────────
# Setup: Parse tag metadata
# ──────────────────────────────────────────────────────────────

setup:
name: Setup
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
version: ${{ steps.meta.outputs.version }}
is_prerelease: ${{ steps.meta.outputs.is_prerelease }}
dry_run: ${{ steps.meta.outputs.dry_run }}
steps:
- name: Resolve tag
id: resolve
run: |
TAG="${{ inputs.tag || github.ref_name }}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Extract metadata from tag
id: meta
env:
TAG: ${{ steps.resolve.outputs.tag }}
run: |
VERSION="${TAG#iii-lsp/v}"

if [[ "$VERSION" =~ -dry-run\.[0-9]+$ ]]; then
echo "dry_run=true" >> "$GITHUB_OUTPUT"
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
elif [[ "$VERSION" =~ -([a-z]+)\.[0-9]+$ ]]; then
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "dry_run=false" >> "$GITHUB_OUTPUT"
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
fi

echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "::notice::iii-lsp release -- tag=$TAG version=$VERSION"
Comment on lines +35 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For softprops/action-gh-release@v2, what happens when tag_name does not already exist? Does it create the tag from the workflow commit?

💡 Result:

If you pass a tag_name that doesn’t already exist, softprops/action-gh-release@v2 will still create the release: it calls GitHub’s “Create a release” API, where tag_name may be an existing tag or a new one, and target_commitish specifies what commit/branch the new tag is created from (and is ignored if the tag already exists). [1]

So it does not automatically create the tag from the workflow run’s commit unless you explicitly set:

  • target_commitish: ${{ github.sha }}

Otherwise, the tag is created from the repo’s default branch by default. [2]

Sources:
[1] GitHub REST API “Create a release” parameters (tag_name, target_commitish) https://docs.github.com/en/rest/releases/releases
[2] softprops/action-gh-release@v2 README (tag_name default, target_commitish behavior/default) https://github.com/softprops/action-gh-release


🏁 Script executed:

# First, let's find and read the workflow file
find . -name "release-lsp-binary.yml" -type f

Repository: iii-hq/workers

Length of output: 100


🏁 Script executed:

# If found, read the entire workflow file to understand context
cat -n .github/workflows/release-lsp-binary.yml

Repository: iii-hq/workers

Length of output: 4363


Validate manual dispatch input against tag format before creating release.

Line 38 accepts arbitrary user input for manual dispatch without format validation. Line 46 blindly strips iii-lsp/v via parameter expansion, producing invalid version metadata. More critically, the resolved tag is passed to softprops/action-gh-release@v2 (line 89) without target_commitish set. This means:

  • A malformed tag like iii-lsp/invalid creates an invalid release
  • A non-existent tag (e.g., manually-created iii-lsp/v1.0.0) is created as a new tag from the default branch, not the workflow commit
  • Release metadata is derived from unvalidated input

Push-triggered events are safe (GitHub enforces iii-lsp/v* pattern), but manual dispatch must validate tag format before processing.

Proposed fix
      - name: Resolve tag
        id: resolve
        run: |
          TAG="${{ inputs.tag || github.ref_name }}"
+         if [[ ! "$TAG" =~ ^iii-lsp/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
+           echo "::error::Invalid tag format: $TAG"
+           exit 1
+         fi
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
+
+      - name: Verify tag exists (manual dispatch)
+        if: github.event_name == 'workflow_dispatch'
+        env:
+          TAG: ${{ steps.resolve.outputs.tag }}
+        run: |
+          git ls-remote --exit-code --tags "https://github.com/${{ github.repository }}.git" "refs/tags/$TAG" > /dev/null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-lsp-binary.yml around lines 35 - 60, The workflow
currently accepts arbitrary manual input in the Resolve tag step (TAG in
steps.resolve) and then parameter-expands away the prefix in Extract metadata
(steps.meta) without validating format, and later passes the tag to
softprops/action-gh-release@v2 without setting target_commitish; fix by
validating the resolved tag in the Resolve step (ensure it matches the expected
pattern like ^iii-lsp/v[0-9]+\.[0-9]+\.[0-9]+(?:-[a-z]+\.[0-9]+|
-dry-run\.[0-9]+)?$ or at minimum starts with "iii-lsp/v" and a semver-like
version) and fail the job early on invalid input, only allow stripping the
"iii-lsp/v" prefix in Extract metadata after validation (use
steps.resolve.outputs.tag), and set the release action's target_commitish to a
deterministic ref (use github.sha for manual_dispatch or github.ref for push
events) when calling softprops/action-gh-release@v2 so malformed or non-existent
tags cannot create releases from the default branch.


# ──────────────────────────────────────────────────────────────
# GitHub Release
# ──────────────────────────────────────────────────────────────

create-release:
name: Create GitHub Release
needs: [setup]
if: needs.setup.outputs.dry_run != 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Generate token
id: generate_token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

- uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
token: ${{ steps.generate_token.outputs.token }}
tag_name: ${{ needs.setup.outputs.tag }}
name: iii-lsp ${{ needs.setup.outputs.version }}
draft: false
prerelease: ${{ needs.setup.outputs.is_prerelease == 'true' }}
generate_release_notes: true

# ──────────────────────────────────────────────────────────────
# Binary Build
# ──────────────────────────────────────────────────────────────

binary-build:
name: Binary Release
needs: [setup, create-release]
if: ${{ !failure() && !cancelled() }}
uses: ./.github/workflows/_rust-binary.yml
with:
bin_name: iii-lsp
manifest_path: iii-lsp/Cargo.toml
tag_name: ${{ needs.setup.outputs.tag }}
is_prerelease: ${{ needs.setup.outputs.is_prerelease == 'true' }}
skip_create_release: true
dry_run: ${{ needs.setup.outputs.dry_run == 'true' }}
secrets:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
Loading