diff --git a/.craft.yml b/.craft.yml new file mode 100644 index 0000000..1d7cfec --- /dev/null +++ b/.craft.yml @@ -0,0 +1,7 @@ +minVersion: 0.23.1 +changelogPolicy: auto +preReleaseCommand: pwsh -cwa '' +artifactProvider: + name: none +targets: + - name: github diff --git a/.github/workflows/danger-workflow-tests.yml b/.github/workflows/danger-workflow-tests.yml index 0b27a3a..3d1b14e 100644 --- a/.github/workflows/danger-workflow-tests.yml +++ b/.github/workflows/danger-workflow-tests.yml @@ -5,14 +5,32 @@ on: pull_request: types: [opened, synchronize, reopened, edited, ready_for_review] -jobs: - danger: - uses: ./.github/workflows/danger.yml - with: - _workflow_version: ${{ github.sha }} +permissions: + contents: read + pull-requests: write + statuses: write - test-outputs: +jobs: + # Test Danger action on pull requests - should analyze PR and report findings + pr-analysis: runs-on: ubuntu-latest - needs: danger steps: - - run: "[[ '${{ needs.danger.outputs.outcome }}' == 'success' ]]" + - uses: actions/checkout@v4 + + - name: Run danger action + id: danger + uses: ./danger + + - name: Validate danger outputs + env: + DANGER_OUTCOME: ${{ steps.danger.outputs.outcome }} + shell: pwsh + run: | + Write-Host "🔍 Validating Danger action outputs..." + Write-Host "Danger Outcome: '$env:DANGER_OUTCOME'" + + # Validate that Danger ran successfully + $env:DANGER_OUTCOME | Should -Be "success" + + Write-Host "✅ Danger PR analysis completed successfully!" + Write-Host "â„šī¸ Check the PR comments for any Danger findings" diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml deleted file mode 100644 index 0d3abb5..0000000 --- a/.github/workflows/danger.yml +++ /dev/null @@ -1,41 +0,0 @@ -# Runs DangerJS with a pre-configured set of rules on a Pull Request. -on: - workflow_call: - inputs: - _workflow_version: - description: 'Internal: specify github-workflows (this repo) revision to use when checking out scripts.' - type: string - required: false - default: v2 # Note: update when publishing a new version - outputs: - outcome: - description: Whether the Danger run finished successfully. Possible values are success, failure, cancelled, or skipped. - value: ${{ jobs.danger.outputs.outcome }} - -jobs: - danger: - runs-on: ubuntu-latest - outputs: - outcome: ${{ steps.danger.outcome }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Download dangerfile.js - run: wget https://raw.githubusercontent.com/getsentry/github-workflows/${{ inputs._workflow_version }}/danger/dangerfile.js -P ${{ runner.temp }} - - # Using a pre-built docker image in GitHub container registry instaed of NPM to reduce possible attack vectors. - - name: Run DangerJS - id: danger - run: | - docker run \ - --volume ${{ github.workspace }}:/github/workspace \ - --volume ${{ runner.temp }}:${{ runner.temp }} \ - --workdir /github/workspace \ - --user $UID \ - -e "INPUT_ARGS" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e GITHUB_ACTIONS=true -e CI=true \ - -e GITHUB_TOKEN="${{ github.token }}" \ - -e DANGER_DISABLE_TRANSPILATION="true" \ - ghcr.io/danger/danger-js:11.3.1 \ - --failOnErrors --dangerfile ${{ runner.temp }}/dangerfile.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ce163ae --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: Release + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + version: + description: Version to release + required: true + force: + description: Force a release even when there are release-blockers (optional) + required: false + +jobs: + release: + runs-on: ubuntu-latest + name: "Release a new version" + steps: + - name: Get auth token + id: token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + with: + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v4 + with: + token: ${{ steps.token.outputs.token }} + fetch-depth: 0 + + - name: Prepare release + uses: getsentry/action-prepare-release@v1 + env: + GITHUB_TOKEN: ${{ steps.token.outputs.token }} + with: + version: ${{ github.event.inputs.version }} + force: ${{ github.event.inputs.force }} \ No newline at end of file diff --git a/.github/workflows/script-tests.yml b/.github/workflows/script-tests.yml index 3a6f16e..2b79699 100644 --- a/.github/workflows/script-tests.yml +++ b/.github/workflows/script-tests.yml @@ -1,5 +1,7 @@ # This isn't a reusable workflow but a CI action for this repo itself - testing the contained workflows & scripts. name: Script Tests +permissions: + contents: read on: push: @@ -23,3 +25,23 @@ jobs: - run: Invoke-Pester working-directory: updater shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + + danger: + name: Danger JS Tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: danger + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '18' + + - run: node --test + + - name: Check syntax + run: node -c dangerfile.js diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml new file mode 100644 index 0000000..6b1eeca --- /dev/null +++ b/.github/workflows/update-deps.yml @@ -0,0 +1,21 @@ +name: Update dependencies + +on: + workflow_dispatch: + schedule: + # Run weekly on Mondays at 8:00 UTC + - cron: '0 8 * * 1' + +permissions: + contents: write + pull-requests: write + +jobs: + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@main + with: + path: danger/danger.properties + name: Danger JS + api-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml deleted file mode 100644 index 7b70200..0000000 --- a/.github/workflows/updater.yml +++ /dev/null @@ -1,258 +0,0 @@ -# Allows updating dependencies to the latest published tag -on: - workflow_call: - inputs: - path: - description: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script. - type: string - required: true - name: - description: Name used in the PR title and the changelog entry. - type: string - required: true - pattern: - description: RegEx pattern that will be matched against available versions when picking the latest one. - type: string - required: false - default: '' - changelog-entry: - description: Whether to add a changelog entry for the update. - type: boolean - required: false - default: true - changelog-section: - description: Section header to attach the changelog entry to. - type: string - required: false - default: Dependencies - runs-on: - description: GitHub Actions virtual environment name to run the udpater job on. - type: string - required: false - default: 'ubuntu-latest' - pr-strategy: - description: | - How to handle PRs - can be either of the following: - * create - create a new PR for new dependency versions as they are released - maintainers may merge or close older PRs manually - * update - keep a single PR that gets updated with new dependency versions until merged - only the latest version update is available at any time - type: string - required: false - default: create - _workflow_version: - description: 'Internal: specify github-workflows (this repo) revision to use when checking out scripts.' - type: string - required: false - default: v2 # Note: update when publishing a new version - secrets: - api-token: - required: true - outputs: - prUrl: - description: 'The created/updated PRs url.' - value: ${{ jobs.update.outputs.prUrl }} - baseBranch: - description: 'The base branch name.' - value: ${{ jobs.update.outputs.baseBranch }} - prBranch: - description: 'The created/updated pr branch name.' - value: ${{ jobs.update.outputs.prBranch }} - originalTag: - description: 'The original tag from which the dependency was updated from.' - value: ${{ jobs.update.outputs.originalTag }} - latestTag: - description: 'The latest tag to which the dependency was updated to.' - value: ${{ jobs.update.outputs.latestTag }} - -jobs: - cancel-previous-run: - runs-on: ubuntu-latest - steps: - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 - with: - access_token: ${{ github.token }} - - # What we need to accomplish: - # * update to the latest tag - # * create a PR - # * update changelog (including the link to the just created PR) - # - # What we actually do is based on whether a PR exists already: - # * YES it does: - # * make the update - # * update changelog (with the ID of an existing PR) - # * push to the PR - # * NO it doesn't: - # * make the update - # * push to a new PR - # * update changelog (with the ID of the just created PR) - # * push to the PR - # We do different approach on subsequent runs because otherwise we would spam users' mailboxes - # with notifications about pushes to existing PRs. This way there is actually no push if not needed. - update: - runs-on: ${{ inputs.runs-on }} - # Map the job outputs to step outputs - outputs: - prUrl: ${{ steps.pr.outputs.url }} - baseBranch: ${{ steps.root.outputs.baseBranch }} - prBranch: ${{ steps.root.outputs.prBranch }} - originalTag: ${{ steps.target.outputs.originalTag }} - latestTag: ${{ steps.target.outputs.latestTag }} - timeout-minutes: 30 - defaults: - run: - shell: pwsh - steps: - - uses: actions/checkout@v4 - with: - ssh-key: ${{ secrets.api-token }} - - # In order to run scripts from this repo, we need to check it out manually, doesn't seem available locally. - - name: Check out workflow scripts - # Note: cannot use `actions/checkout` at the moment because you can't clone outside of the repo root. - # Follow https://github.com/actions/checkout/issues/197 - run: | - mkdir -p ${{ runner.temp }}/ghwf - cd ${{ runner.temp }}/ghwf - git init - git remote add origin https://github.com/getsentry/github-workflows.git - git fetch --depth 1 origin ${{ inputs._workflow_version }} - git checkout FETCH_HEAD - - - name: Update to the latest version - id: target - run: ${{ runner.temp }}/ghwf/updater/scripts/update-dependency.ps1 -Path '${{ inputs.path }}' -Pattern '${{ inputs.pattern }}' - - - name: Get the base repo info - if: steps.target.outputs.latestTag != steps.target.outputs.originalTag - id: root - run: | - $mainBranch = $(git remote show origin | Select-String "HEAD branch: (.*)").Matches[0].Groups[1].Value - $prBranch = switch ('${{ inputs.pr-strategy }}') - { - 'create' { 'deps/${{ inputs.path }}/${{ steps.target.outputs.latestTag }}' } - 'update' { 'deps/${{ inputs.path }}' } - default { throw "Unkown PR strategy '${{ inputs.pr-strategy }}'." } - } - "baseBranch=$mainBranch" | Tee-Object $env:GITHUB_OUTPUT -Append - "prBranch=$prBranch" | Tee-Object $env:GITHUB_OUTPUT -Append - $nonBotCommits = ${{ runner.temp }}/ghwf/updater/scripts/nonbot-commits.ps1 ` - -RepoUrl "$(git config --get remote.origin.url)" -PrBranch $prBranch -MainBranch $mainBranch - $changed = $nonBotCommits.Length -gt 0 ? 'true' : 'false' - "changed=$changed" | Tee-Object $env:GITHUB_OUTPUT -Append - if ("$changed" -eq "true") - { - Write-Output "::warning::Target branch '$prBranch' has been changed manually - skipping updater to avoid overwriting these changes." - } - - - name: Parse the existing PR URL - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - id: existing-pr - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - $urls = @(gh api 'repos/${{ github.repository }}/pulls?base=${{ steps.root.outputs.baseBranch }}&head=${{ github.repository_owner }}:${{ steps.root.outputs.prBranch }}' --jq '.[].html_url') - if ($urls.Length -eq 0) - { - "url=" | Tee-Object $env:GITHUB_OUTPUT -Append - } - elseif ($urls.Length -eq 1) - { - "url=$($urls[0])" | Tee-Object $env:GITHUB_OUTPUT -Append - } - else - { - throw "Unexpected number of PRs matched ($($urls.Length)): $urls" - } - - - run: git --no-pager diff - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} - - - name: Get target changelog - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - run: | - $changelog = ${{ runner.temp }}/ghwf/updater/scripts/get-changelog.ps1 ` - -RepoUrl '${{ steps.target.outputs.url }}' ` - -OldTag '${{ steps.target.outputs.originalTag }}' ` - -NewTag '${{ steps.target.outputs.latestTag }}' - ${{ runner.temp }}/ghwf/updater/scripts/set-github-env.ps1 TARGET_CHANGELOG $changelog - - # First we create a PR only if it doesn't exist. We will later overwrite the content with the same action. - - name: Create a PR - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} - uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # pin#v6.0.1 - id: create-pr - with: - base: ${{ steps.root.outputs.baseBranch }} - branch: ${{ steps.root.outputs.prBranch }} - commit-message: 'chore: update ${{ inputs.path }} to ${{ steps.target.outputs.latestTag }}' - author: 'GitHub ' - title: 'chore(deps): update ${{ inputs.name }} to ${{ steps.target.outputs.latestTagNice }}' - body: | - Bumps ${{ inputs.path }} from ${{ steps.target.outputs.originalTag }} to ${{ steps.target.outputs.latestTag }}. - - Auto-generated by a [dependency updater](https://github.com/getsentry/github-workflows/blob/main/.github/workflows/updater.yml). - ${{ env.TARGET_CHANGELOG }} - labels: dependencies - # draft: true - - - name: Verify we have a PR - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - id: pr - run: | - if ('${{ steps.create-pr.outputs.pull-request-url }}' -ne '') - { - "url=${{ steps.create-pr.outputs.pull-request-url }}" | Tee-Object $env:GITHUB_OUTPUT -Append - } - elseif ('${{ steps.existing-pr.outputs.url }}' -ne '') - { - "url=${{ steps.existing-pr.outputs.url }}" | Tee-Object $env:GITHUB_OUTPUT -Append - } - else - { - throw "PR hasn't been created" - } - - # If we had to create a new PR, we must do a clean checkout & update the submodule again. - # If we didn't do this, the new PR would only have a changelog... - - name: 'After new PR: restore repo' - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} - uses: actions/checkout@v4 - with: - ssh-key: ${{ secrets.api-token }} - - - name: 'After new PR: redo the update' - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} - run: ${{ runner.temp }}/ghwf/updater/scripts/update-dependency.ps1 -Path '${{ inputs.path }}' -Tag '${{ steps.target.outputs.latestTag }}' - - - name: Update Changelog - if: ${{ inputs.changelog-entry && ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - run: | - ${{ runner.temp }}/ghwf/updater/scripts/update-changelog.ps1 ` - -Name '${{ inputs.name }}' ` - -PR '${{ steps.pr.outputs.url }}' ` - -RepoUrl '${{ steps.target.outputs.url }}' ` - -MainBranch '${{ steps.target.outputs.mainBranch }}' ` - -OldTag '${{ steps.target.outputs.originalTag }}' ` - -NewTag '${{ steps.target.outputs.latestTag }}' ` - -Section '${{ inputs.changelog-section }}' - - - run: git --no-pager diff - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - - # Now make the PR in its final state. This way we only have one commit and no updates if there are no changes between runs. - - name: Update the PR - if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} - uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # pin#v6.0.1 - with: - base: ${{ steps.root.outputs.baseBranch }} - branch: ${{ steps.root.outputs.prBranch }} - commit-message: 'chore: update ${{ inputs.path }} to ${{ steps.target.outputs.latestTag }}' - author: 'GitHub ' - title: 'chore(deps): update ${{ inputs.name }} to ${{ steps.target.outputs.latestTagNice }}' - body: | - Bumps ${{ inputs.path }} from ${{ steps.target.outputs.originalTag }} to ${{ steps.target.outputs.latestTag }}. - - Auto-generated by a [dependency updater](https://github.com/getsentry/github-workflows/blob/main/.github/workflows/updater.yml). - ${{ env.TARGET_CHANGELOG }} - labels: dependencies diff --git a/.github/workflows/versioning.yml b/.github/workflows/versioning.yml index 9f26dbc..7b5f58f 100644 --- a/.github/workflows/versioning.yml +++ b/.github/workflows/versioning.yml @@ -1,8 +1,9 @@ name: Sync tags with releases on: - release: - types: [published, edited] + workflow_dispatch: + # release: + # types: [published, edited] jobs: actions-tagger: diff --git a/.github/workflows/workflow-tests.yml b/.github/workflows/workflow-tests.yml index 5867c33..e804e2d 100644 --- a/.github/workflows/workflow-tests.yml +++ b/.github/workflows/workflow-tests.yml @@ -4,46 +4,158 @@ name: Workflow Tests on: push: +permissions: + contents: write + pull-requests: write + actions: write + jobs: - updater-create-pr: - uses: ./.github/workflows/updater.yml - with: - path: updater/tests/sentry-cli.properties - name: WORKFLOW-TEST-DEPENDENCY-DO-NOT-MERGE - pattern: '^2\.0\.' - pr-strategy: update - _workflow_version: ${{ github.sha }} - secrets: - api-token: ${{ github.token }} - - updater-test-args: - uses: ./.github/workflows/updater.yml - with: - path: updater/tests/workflow-args.sh - name: Workflow args test script - runs-on: macos-latest - pattern: '.*' - _workflow_version: ${{ github.sha }} - secrets: - api-token: ${{ github.token }} - - updater-test-outputs: + # Test PR creation scenario - should create a PR with specific version pattern + updater-pr-creation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run updater action + id: updater + uses: ./updater + with: + path: updater/tests/sentry-cli.properties + name: WORKFLOW-TEST-DEPENDENCY-DO-NOT-MERGE + pattern: '^2\.0\.' + pr-strategy: update + api-token: ${{ github.token }} + + - name: Validate PR creation outputs + env: + BASE_BRANCH: ${{ steps.updater.outputs.baseBranch }} + ORIGINAL_TAG: ${{ steps.updater.outputs.originalTag }} + LATEST_TAG: ${{ steps.updater.outputs.latestTag }} + PR_URL: ${{ steps.updater.outputs.prUrl }} + PR_BRANCH: ${{ steps.updater.outputs.prBranch }} + shell: pwsh + run: | + Write-Host "🔍 Validating PR creation scenario outputs..." + Write-Host "Base Branch: '$env:BASE_BRANCH'" + Write-Host "Original Tag: '$env:ORIGINAL_TAG'" + Write-Host "Latest Tag: '$env:LATEST_TAG'" + Write-Host "PR URL: '$env:PR_URL'" + Write-Host "PR Branch: '$env:PR_BRANCH'" + + # Validate base branch is main + $env:BASE_BRANCH | Should -Be "main" + + # Validate original tag is expected test value + $env:ORIGINAL_TAG | Should -Be "2.0.0" + + # Validate latest tag is a valid version + $env:LATEST_TAG | Should -Match "^[0-9]+\.[0-9]+\.[0-9]+$" + + # Validate PR URL format + $env:PR_URL | Should -Match "^https://github\.com/getsentry/github-workflows/pull/[0-9]+$" + + # Validate PR branch format + $env:PR_BRANCH | Should -Be "deps/updater/tests/sentry-cli.properties" + + Write-Host "✅ PR creation scenario validation passed!" + + # Test target-branch functionality - should use specified branch as base + updater-target-branch: runs-on: ubuntu-latest - needs: - - updater-create-pr - - updater-test-args steps: - - run: "[[ '${{ needs.updater-create-pr.outputs.baseBranch }}' == 'main' ]]" - - run: "[[ '${{ needs.updater-create-pr.outputs.originalTag }}' == '2.0.0' ]]" - - run: "[[ '${{ needs.updater-create-pr.outputs.latestTag }}' =~ ^[0-9.]+$ ]]" - - run: "[[ '${{ needs.updater-create-pr.outputs.prUrl }}' =~ ^https://github.com/getsentry/github-workflows/pull/[0-9]+$ ]]" - - run: "[[ '${{ needs.updater-create-pr.outputs.prBranch }}' == 'deps/updater/tests/sentry-cli.properties' ]]" - - - run: "[[ '${{ needs.updater-test-args.outputs.baseBranch }}' == '' ]]" - - run: "[[ '${{ needs.updater-test-args.outputs.originalTag }}' == 'latest' ]]" - - run: "[[ '${{ needs.updater-test-args.outputs.latestTag }}' == 'latest' ]]" - - run: "[[ '${{ needs.updater-test-args.outputs.prUrl }}' == '' ]]" - - run: "[[ '${{ needs.updater-test-args.outputs.prBranch }}' == '' ]]" + - uses: actions/checkout@v4 + + - name: Run updater action with target-branch + id: updater + uses: ./updater + with: + path: updater/tests/sentry-cli.properties + name: TARGET-BRANCH-TEST-DO-NOT-MERGE + pattern: '^2\.0\.' + target-branch: test/nonbot-commits + pr-strategy: update + api-token: ${{ github.token }} + + - name: Validate target-branch outputs + env: + BASE_BRANCH: ${{ steps.updater.outputs.baseBranch }} + ORIGINAL_TAG: ${{ steps.updater.outputs.originalTag }} + LATEST_TAG: ${{ steps.updater.outputs.latestTag }} + PR_URL: ${{ steps.updater.outputs.prUrl }} + PR_BRANCH: ${{ steps.updater.outputs.prBranch }} + shell: pwsh + run: | + Write-Host "🔍 Validating target-branch scenario outputs..." + Write-Host "Base Branch: '$env:BASE_BRANCH'" + Write-Host "Original Tag: '$env:ORIGINAL_TAG'" + Write-Host "Latest Tag: '$env:LATEST_TAG'" + Write-Host "PR URL: '$env:PR_URL'" + Write-Host "PR Branch: '$env:PR_BRANCH'" + + # Validate base branch is the specified target-branch + $env:BASE_BRANCH | Should -Be "test/nonbot-commits" + + # Validate original tag is expected test value + $env:ORIGINAL_TAG | Should -Be "2.0.0" + + # Validate latest tag is a valid version + $env:LATEST_TAG | Should -Match "^[0-9]+\.[0-9]+\.[0-9]+$" + + # Validate PR URL format + $env:PR_URL | Should -Match "^https://github\.com/getsentry/github-workflows/pull/[0-9]+$" + + # Validate PR branch format + $env:PR_BRANCH | Should -Be "test/nonbot-commits-deps/updater/tests/sentry-cli.properties" + + Write-Host "✅ Target-branch scenario validation passed!" + + # Test no-change scenario - should detect no updates needed + updater-no-changes: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Run updater action + id: updater + uses: ./updater + with: + path: updater/tests/workflow-args.sh + name: Workflow args test script + pattern: '.*' + api-token: ${{ github.token }} + + - name: Validate no-changes outputs + env: + BASE_BRANCH: ${{ steps.updater.outputs.baseBranch }} + ORIGINAL_TAG: ${{ steps.updater.outputs.originalTag }} + LATEST_TAG: ${{ steps.updater.outputs.latestTag }} + PR_URL: ${{ steps.updater.outputs.prUrl }} + PR_BRANCH: ${{ steps.updater.outputs.prBranch }} + shell: pwsh + run: | + Write-Host "🔍 Validating no-changes scenario outputs..." + Write-Host "Base Branch: '$env:BASE_BRANCH'" + Write-Host "Original Tag: '$env:ORIGINAL_TAG'" + Write-Host "Latest Tag: '$env:LATEST_TAG'" + Write-Host "PR URL: '$env:PR_URL'" + Write-Host "PR Branch: '$env:PR_BRANCH'" + + # Validate no PR was created (empty values) + $env:BASE_BRANCH | Should -BeNullOrEmpty + + $env:PR_URL | Should -BeNullOrEmpty + + $env:PR_BRANCH | Should -BeNullOrEmpty + + # Validate original equals latest (no update) + $env:ORIGINAL_TAG | Should -Be $env:LATEST_TAG + + # Validate tag format (should be 'latest' or valid version) + if ($env:ORIGINAL_TAG -ne "latest") { + $env:ORIGINAL_TAG | Should -Match "^v?[0-9]+\.[0-9]+\.[0-9]+$" + } + + Write-Host "✅ No-changes scenario validation passed!" cli-integration: runs-on: ${{ matrix.host }}-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b470d7..0c922a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,124 @@ # Changelog +## 3.1.0 + +### Features + +- Updater - Add `post-update-script` input parameter to run custom scripts after dependency updates ([#130](https://github.com/getsentry/github-workflows/pull/130), [#133](https://github.com/getsentry/github-workflows/pull/133)) + - Scripts receive original and new version as arguments + - Support both bash (`.sh`) and PowerShell (`.ps1`) scripts + - Enables workflows like updating lock files, running code generators, or modifying configuration files +- Updater - Add SSH key support and comprehensive authentication validation ([#134](https://github.com/getsentry/github-workflows/pull/134)) + - Add `ssh-key` input parameter for deploy key authentication + - Support using both `ssh-key` (for git) and `api-token` (for GitHub API) together + - Add detailed token validation with actionable error messages + - Detect common token issues: expiration, whitespace, SSH keys in wrong input, missing scopes + - Validate SSH key format when provided + +### Fixes + +- Updater - Fix boolean input handling for `changelog-entry` parameter and add input validation ([#127](https://github.com/getsentry/github-workflows/pull/127)) +- Updater - Fix cryptic authentication errors with better validation and error messages ([#134](https://github.com/getsentry/github-workflows/pull/134), closes [#128](https://github.com/getsentry/github-workflows/issues/128)) + +### Dependencies + +- Bump Danger JS from v11.3.1 to v13.0.4 ([#132](https://github.com/getsentry/github-workflows/pull/132)) + - [changelog](https://github.com/danger/danger-js/blob/main/CHANGELOG.md#1304) + - [diff](https://github.com/danger/danger-js/compare/11.3.1...13.0.4) + +## 3.0.0 + +### Breaking Changes + +- Updater: The default value for `pr-strategy` has been changed from `create` to `update`. ([#124](https://github.com/getsentry/github-workflows/pull/124)) + This change means the updater will now maintain a single PR that gets updated with new dependency versions (instead of creating separate PRs for each version). + If you want to preserve the previous behavior of creating separate PRs, explicitly set `pr-strategy: create` in your workflow: + + ```yaml + - uses: getsentry/github-workflows/updater@v3 + with: + # ... other inputs ... + pr-strategy: create # Add this to preserve previous behavior + ``` + + In case you have existing open PRs created with the `create` strategy, you will need to remove these old branches + manually as the new name would be a prefix of the old PRs, which git doesnt' allow. + +- Updater and Danger reusable workflows are now composite actions ([#114](https://github.com/getsentry/github-workflows/pull/114)) + + To update your existing Updater workflows: + + ```yaml + ### Before + native: + uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + secrets: + # If a custom token is used instead, a CI would be triggered on a created PR. + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + ### After (v3.0) + native: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + api-token: ${{ secrets.CI_DEPLOY_KEY }} + ``` + + **Note**: If you were using SSH deploy keys with the v2 reusable workflow, the v3.0 composite action initially only supported tokens. + SSH key support was restored in v3.1 ([#134](https://github.com/getsentry/github-workflows/pull/134)). To use SSH keys, update to v3.1+ and use the `ssh-key` input: + + ```yaml + ### With SSH key (v3.1+) + native: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + ssh-key: ${{ secrets.CI_DEPLOY_KEY }} + ``` + + To update your existing Danger workflows: + + ```yaml + ### Before + danger: + uses: getsentry/github-workflows/.github/workflows/danger.yml@v2 + + ### After + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 + ``` + +### Features + +- Updater now supports targeting non-default branches via the new `target-branch` input parameter ([#118](https://github.com/getsentry/github-workflows/pull/118)) +- Updater now supports filtering releases by GitHub release title patterns, e.g. to support release channels ([#117](https://github.com/getsentry/github-workflows/pull/117)) +- Updater now supports dependencies without changelog files by falling back to git commit messages ([#116](https://github.com/getsentry/github-workflows/pull/116)) +- Danger - Improve conventional commit scope handling, and non-conventional PR title support ([#105](https://github.com/getsentry/github-workflows/pull/105)) +- Add Proguard artifact endpoint for Android builds in sentry-server ([#100](https://github.com/getsentry/github-workflows/pull/100)) +- Updater - Add CMake FetchContent support for automated dependency updates ([#104](https://github.com/getsentry/github-workflows/pull/104)) + +### Security + +- Updater - Prevent script injection vulnerabilities through workflow inputs ([#98](https://github.com/getsentry/github-workflows/pull/98)) + +### Fixes + +- Updater - Fix null reference error when changelog has no existing bullet points ([#125](https://github.com/getsentry/github-workflows/pull/125)) +- Updater - Fix bullet-point resolution when plain text precedes bullet points ([#123](https://github.com/getsentry/github-workflows/pull/123)) +- Improve changelog generation for non-tagged commits and edge cases ([#115](https://github.com/getsentry/github-workflows/pull/115)) +- Use GITHUB_WORKFLOW_REF instead of _workflow_version input parameter to automatically determine workflow script versions ([#109](https://github.com/getsentry/github-workflows/pull/109)) + ## 2.13.1 ### Fixes diff --git a/README.md b/README.md index 48d4e1b..9c7cfc3 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,23 @@ -# Workflows +# GitHub Workflows -This repository contains reusable workflows and scripts to be used with GitHub Actions. +This repository contains composite actions and scripts to be used with GitHub Actions. -## Updater +## Composite Actions -Dependency updater - see [updater.yml](.github/workflows/updater.yml) - updates dependencies to the latest published git tag. +### Updater -### Example workflow definition +Dependency updater - updates dependencies to the latest published git tag and creates/updates PRs. -```yaml -name: Update Dependencies -on: - # Run every day. - schedule: - - cron: '0 3 * * *' - # And on on every PR merge so we get the updated dependencies ASAP, and to make sure the changelog doesn't conflict. - push: - branches: - - main -jobs: - # Update a git submodule - cocoa: - uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 - with: - path: modules/sentry-cocoa - name: Cocoa SDK - pattern: '^1\.' # Limit to major version '1' - secrets: - api-token: ${{ secrets.CI_DEPLOY_KEY }} +**[📖 View full documentation →](updater/README.md)** - # Update a properties file - cli: - uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 - with: - path: sentry-cli.properties - name: CLI - secrets: - api-token: ${{ secrets.CI_DEPLOY_KEY }} +### Danger - # Update using a custom shell script, see updater/scripts/update-dependency.ps1 for the required arguments - agp: - uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 - with: - path: script.ps1 - name: Gradle Plugin - secrets: - api-token: ${{ secrets.CI_DEPLOY_KEY }} -``` +Runs DangerJS on Pull Requests with a pre-configured set of rules. -### Inputs +**[📖 View full documentation →](danger/README.md)** -* `path`: Dependency path in the source repository, this can be either a submodule, a .properties file or a shell script. - * type: string - * required: true -* `name`: Name used in the PR title and the changelog entry. - * type: string - * required: true -* `pattern`: RegEx pattern that will be matched against available versions when picking the latest one. - * type: string - * required: false - * default: '' -* `changelog-entry`: Whether to add a changelog entry for the update. - * type: boolean - * required: false - * default: true -* `changelog-section`: Section header to attach the changelog entry to. - * type: string - * required: false - * default: Dependencies -* `runs-on`: GitHub Actions virtual environment name to run the udpater job on. - * type: string - * required: false - * default: ubuntu-latest -* `pr-strategy`: How to handle PRs. - Can be either of the following: - * `create` (default) - create a new PR for new dependency versions as they are released - maintainers may merge or close older PRs manually - * `update` - keep a single PR that gets updated with new dependency versions until merged - only the latest version update is available at any time +## Legacy Reusable Workflows (v2) -### Secrets +> âš ī¸ **Deprecated**: Reusable workflows have been converted to composite actions in v3. Please migrate to the composite actions above. -* `api-token`: GH authentication token to create PRs with & push. - If you provide the usual `${{github.token}}`, no followup CI will run on the created PR. - If you want CI to run on the PRs created by the Updater, you need to provide custom user-specific auth token. - -## Danger - -Runs DangerJS on Pull Reqeusts in your repository. This uses custom set of rules defined in [this dangerfile](danger/dangerfile.js). - -```yaml -name: Danger - -on: - pull_request: - types: [opened, synchronize, reopened, edited, ready_for_review, labeled, unlabeled] - -jobs: - danger: - uses: getsentry/github-workflows/.github/workflows/danger.yml@v2 -``` +For v2 migration guide and breaking changes, see [CHANGELOG.md](CHANGELOG.md#3.0.0). diff --git a/danger/README.md b/danger/README.md new file mode 100644 index 0000000..daaee7d --- /dev/null +++ b/danger/README.md @@ -0,0 +1,55 @@ +# Danger Composite Action + +Runs DangerJS on Pull Requests in your repository. This uses custom set of rules defined in [dangerfile.js](dangerfile.js). + +## Usage + +```yaml +name: Danger + +on: + pull_request: + types: [opened, synchronize, reopened, edited, ready_for_review, labeled, unlabeled] + +permissions: + contents: read # To read repository files + pull-requests: write # To post comments on pull requests + statuses: write # To post commit status checks + +jobs: + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 +``` + +## Inputs + +* `api-token`: Token for the repo. Can be passed in using `${{ secrets.GITHUB_TOKEN }}`. + * type: string + * required: false + * default: `${{ github.token }}` + +## Outputs + +* `outcome`: Whether the Danger run finished successfully. Possible values are `success`, `failure`, `cancelled`, or `skipped`. + +## Migration from v2 Reusable Workflow + +If you're migrating from the v2 reusable workflow, see the [changelog migration guide](../CHANGELOG.md#unreleased) for detailed examples. + +Key changes: +- Add `runs-on` to specify the runner +- No need for explicit `actions/checkout` step (handled internally) +- Optional `api-token` input (defaults to `github.token`) + +## Rules + +The Danger action runs the following checks: + +- **Changelog validation**: Ensures PRs include appropriate changelog entries +- **Action pinning**: Verifies GitHub Actions are pinned to specific commits for security +- **Conventional commits**: Validates commit message format and PR title conventions +- **Cross-repo links**: Checks for proper formatting of links in changelog entries + +For detailed rule implementations, see [dangerfile.js](dangerfile.js). \ No newline at end of file diff --git a/danger/action.yml b/danger/action.yml new file mode 100644 index 0000000..56d222b --- /dev/null +++ b/danger/action.yml @@ -0,0 +1,46 @@ +name: 'Danger JS' +description: 'Runs DangerJS with a pre-configured set of rules on a Pull Request' +author: 'Sentry' + +inputs: + api-token: + description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' + required: false + default: ${{ github.token }} + +outputs: + outcome: + description: 'Whether the Danger run finished successfully. Possible values are success, failure, cancelled, or skipped.' + value: ${{ steps.danger.outcome }} + +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ inputs.api-token }} + fetch-depth: 0 + + # Read the Danger version from the properties file + - name: Get Danger version + id: config + shell: pwsh + run: Get-Content '${{ github.action_path }}/danger.properties' | Tee-Object $env:GITHUB_OUTPUT -Append + + # Using a pre-built docker image in GitHub container registry instead of NPM to reduce possible attack vectors. + - name: Run DangerJS + id: danger + shell: bash + run: | + docker run \ + --volume ${{ github.workspace }}:/github/workspace \ + --volume ${{ github.action_path }}:${{ github.action_path }} \ + --volume ${{ github.event_path }}:${{ github.event_path }} \ + --workdir /github/workspace \ + --user $(id -u) \ + -e "INPUT_ARGS" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e GITHUB_ACTIONS=true -e CI=true \ + -e GITHUB_TOKEN="${{ inputs.api-token }}" \ + -e DANGER_DISABLE_TRANSPILATION="true" \ + ghcr.io/danger/danger-js:${{ steps.config.outputs.version }} \ + --failOnErrors --dangerfile ${{ github.action_path }}/dangerfile.js diff --git a/danger/danger.properties b/danger/danger.properties new file mode 100644 index 0000000..466f774 --- /dev/null +++ b/danger/danger.properties @@ -0,0 +1,2 @@ +version=13.0.4 +repo=https://github.com/danger/danger-js diff --git a/danger/dangerfile-utils.js b/danger/dangerfile-utils.js new file mode 100644 index 0000000..daaed77 --- /dev/null +++ b/danger/dangerfile-utils.js @@ -0,0 +1,93 @@ +/// Unified configuration for PR flavors (based on real Sentry usage analysis) +const FLAVOR_CONFIG = [ + { + labels: ["feat", "feature", "add", "implement"], + changelog: "Features", + isFeature: true + }, + { + labels: ["fix", "bug", "bugfix", "resolve", "correct"], + changelog: "Fixes" + }, + { + labels: ["sec", "security"], + changelog: "Security" + }, + { + labels: ["perf", "performance"], + changelog: "Performance" + }, + { + // Internal changes - no changelog needed + changelog: undefined, + labels: [ + "docs", + "doc", + "style", + "ref", + "refactor", + "tests", + "test", + "build", + "ci", + "chore", + "meta", + "deps", + "dep", + "update", + "bump", + "cleanup", + "format" + ] + } +]; + +/// Get flavor configuration for a given PR flavor +function getFlavorConfig(prFlavor) { + const normalizedFlavor = prFlavor.toLowerCase().trim(); + + // Strip scope/context from conventional commit format: "type(scope)" -> "type" + const parenIndex = normalizedFlavor.indexOf('('); + const baseType = parenIndex !== -1 ? normalizedFlavor.substring(0, parenIndex) : normalizedFlavor; + + const config = FLAVOR_CONFIG.find(config => + config.labels.includes(normalizedFlavor) || config.labels.includes(baseType) + ); + + return config || { + changelog: "Features" // Default to Features + }; +} + + +/// Extract PR flavor from title or branch name +function extractPRFlavor(prTitle, prBranchRef) { + // Validate input parameters to prevent runtime errors + if (prTitle && typeof prTitle === 'string') { + // First try conventional commit format: "type(scope): description" + const colonParts = prTitle.split(":"); + if (colonParts.length > 1) { + return colonParts[0].toLowerCase().trim(); + } + + // Fallback: try first word for non-conventional titles like "fix memory leak" + const firstWord = prTitle.trim().split(/\s+/)[0]; + if (firstWord) { + return firstWord.toLowerCase(); + } + } + + if (prBranchRef && typeof prBranchRef === 'string') { + const parts = prBranchRef.split("/"); + if (parts.length > 1) { + return parts[0].toLowerCase(); + } + } + return ""; +} + +module.exports = { + FLAVOR_CONFIG, + getFlavorConfig, + extractPRFlavor +}; diff --git a/danger/dangerfile-utils.test.js b/danger/dangerfile-utils.test.js new file mode 100644 index 0000000..cfd1fe1 --- /dev/null +++ b/danger/dangerfile-utils.test.js @@ -0,0 +1,278 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const { getFlavorConfig, extractPRFlavor, FLAVOR_CONFIG } = require('./dangerfile-utils.js'); + +describe('dangerfile-utils', () => { + describe('getFlavorConfig', () => { + it('should return config for features with isFeature true', () => { + const featConfig = getFlavorConfig('feat'); + assert.strictEqual(featConfig.changelog, 'Features'); + assert.strictEqual(featConfig.isFeature, true); + + const featureConfig = getFlavorConfig('feature'); + assert.strictEqual(featureConfig.changelog, 'Features'); + assert.strictEqual(featureConfig.isFeature, true); + }); + + it('should return config for fixes without isFeature', () => { + const fixConfig = getFlavorConfig('fix'); + assert.strictEqual(fixConfig.changelog, 'Fixes'); + assert.strictEqual(fixConfig.isFeature, undefined); + + const bugConfig = getFlavorConfig('bug'); + assert.strictEqual(bugConfig.changelog, 'Fixes'); + assert.strictEqual(bugConfig.isFeature, undefined); + + const bugfixConfig = getFlavorConfig('bugfix'); + assert.strictEqual(bugfixConfig.changelog, 'Fixes'); + assert.strictEqual(bugfixConfig.isFeature, undefined); + }); + + it('should return config with undefined changelog for skipped flavors', () => { + const skipFlavors = ['docs', 'doc', 'ci', 'tests', 'test', 'style', 'refactor', 'build', 'chore', 'meta', 'deps', 'dep', 'chore(deps)', 'build(deps)']; + + skipFlavors.forEach(flavor => { + const config = getFlavorConfig(flavor); + assert.strictEqual(config.changelog, undefined, `${flavor} should have undefined changelog`); + assert.strictEqual(config.isFeature, undefined, `${flavor} should have undefined isFeature`); + }); + }); + + it('should return default config for unknown flavors', () => { + const unknownConfig = getFlavorConfig('unknown'); + assert.strictEqual(unknownConfig.changelog, 'Features'); + assert.strictEqual(unknownConfig.isFeature, undefined); + + const emptyConfig = getFlavorConfig(''); + assert.strictEqual(emptyConfig.changelog, 'Features'); + assert.strictEqual(emptyConfig.isFeature, undefined); + }); + + it('should be case-insensitive and handle whitespace', () => { + const config1 = getFlavorConfig('FEAT'); + assert.strictEqual(config1.changelog, 'Features'); + + const config2 = getFlavorConfig(' fix '); + assert.strictEqual(config2.changelog, 'Fixes'); + }); + + it('should handle all security-related flavors', () => { + const secConfig = getFlavorConfig('sec'); + assert.strictEqual(secConfig.changelog, 'Security'); + + const securityConfig = getFlavorConfig('security'); + assert.strictEqual(securityConfig.changelog, 'Security'); + }); + + it('should handle all performance-related flavors', () => { + const perfConfig = getFlavorConfig('perf'); + assert.strictEqual(perfConfig.changelog, 'Performance'); + + const performanceConfig = getFlavorConfig('performance'); + assert.strictEqual(performanceConfig.changelog, 'Performance'); + }); + + it('should handle ref flavor (internal changes - no changelog)', () => { + const refConfig = getFlavorConfig('ref'); + assert.strictEqual(refConfig.changelog, undefined); + assert.strictEqual(refConfig.isFeature, undefined); + }); + + it('should handle scoped flavors by stripping scope', () => { + const scopedFeat = getFlavorConfig('feat(core)'); + assert.strictEqual(scopedFeat.changelog, 'Features'); + assert.strictEqual(scopedFeat.isFeature, true); + + const scopedFix = getFlavorConfig('fix(browser)'); + assert.strictEqual(scopedFix.changelog, 'Fixes'); + assert.strictEqual(scopedFix.isFeature, undefined); + + const scopedChore = getFlavorConfig('chore(deps)'); + assert.strictEqual(scopedChore.changelog, undefined); + + // Test edge cases for scope stripping + const nestedParens = getFlavorConfig('feat(scope(nested))'); + assert.strictEqual(nestedParens.changelog, 'Features'); // Should strip at first ( + + const noCloseParen = getFlavorConfig('feat(scope'); + assert.strictEqual(noCloseParen.changelog, 'Features'); // Should still work + + const multipleParens = getFlavorConfig('feat(scope1)(scope2)'); + assert.strictEqual(multipleParens.changelog, 'Features'); // Should strip at first ( + }); + + it('should handle non-conventional action words', () => { + // Feature-related words + const addConfig = getFlavorConfig('add'); + assert.strictEqual(addConfig.changelog, 'Features'); + assert.strictEqual(addConfig.isFeature, true); + + const implementConfig = getFlavorConfig('implement'); + assert.strictEqual(implementConfig.changelog, 'Features'); + assert.strictEqual(implementConfig.isFeature, true); + + // Fix-related words + const resolveConfig = getFlavorConfig('resolve'); + assert.strictEqual(resolveConfig.changelog, 'Fixes'); + + const correctConfig = getFlavorConfig('correct'); + assert.strictEqual(correctConfig.changelog, 'Fixes'); + + // Internal change words + const updateConfig = getFlavorConfig('update'); + assert.strictEqual(updateConfig.changelog, undefined); + + const bumpConfig = getFlavorConfig('bump'); + assert.strictEqual(bumpConfig.changelog, undefined); + + const cleanupConfig = getFlavorConfig('cleanup'); + assert.strictEqual(cleanupConfig.changelog, undefined); + + const formatConfig = getFlavorConfig('format'); + assert.strictEqual(formatConfig.changelog, undefined); + }); + }); + + describe('extractPRFlavor', () => { + it('should extract flavor from PR title with colon', () => { + const flavor = extractPRFlavor('feat: add new feature', null); + assert.strictEqual(flavor, 'feat'); + + const flavor2 = extractPRFlavor('Fix: resolve bug in authentication', null); + assert.strictEqual(flavor2, 'fix'); + + const flavor3 = extractPRFlavor('Docs: Update readme', null); + assert.strictEqual(flavor3, 'docs'); + }); + + it('should extract flavor from branch name with slash', () => { + const flavor = extractPRFlavor(null, 'feature/new-api'); + assert.strictEqual(flavor, 'feature'); + + const flavor2 = extractPRFlavor(null, 'ci/update-workflows'); + assert.strictEqual(flavor2, 'ci'); + + const flavor3 = extractPRFlavor(null, 'fix/auth-bug'); + assert.strictEqual(flavor3, 'fix'); + }); + + it('should prefer title over branch if both available', () => { + const flavor = extractPRFlavor('feat: add feature', 'ci/update-workflows'); + assert.strictEqual(flavor, 'feat'); + }); + + it('should return empty string if no flavor found', () => { + // Empty or whitespace-only strings + const flavor1 = extractPRFlavor('', null); + assert.strictEqual(flavor1, ''); + + const flavor2 = extractPRFlavor(' ', null); + assert.strictEqual(flavor2, ''); + + // No branch with slash + const flavor3 = extractPRFlavor(null, 'simple-branch'); + assert.strictEqual(flavor3, ''); + + // All null/undefined + const flavor4 = extractPRFlavor(null, null); + assert.strictEqual(flavor4, ''); + }); + + it('should handle edge cases', () => { + const flavor1 = extractPRFlavor(':', null); + assert.strictEqual(flavor1, ''); + + const flavor2 = extractPRFlavor(null, '/'); + assert.strictEqual(flavor2, ''); + + const flavor3 = extractPRFlavor('title: with: multiple: colons', null); + assert.strictEqual(flavor3, 'title'); + }); + + it('should validate input parameters and handle non-string types', () => { + // Number inputs + const flavor1 = extractPRFlavor(123, 456); + assert.strictEqual(flavor1, ''); + + // Object inputs + const flavor2 = extractPRFlavor({ test: 'object' }, ['array']); + assert.strictEqual(flavor2, ''); + + // Boolean inputs + const flavor3 = extractPRFlavor(true, false); + assert.strictEqual(flavor3, ''); + + // Mixed valid/invalid inputs + const flavor4 = extractPRFlavor(null, 'valid/branch'); + assert.strictEqual(flavor4, 'valid'); + + const flavor5 = extractPRFlavor('valid: title', 42); + assert.strictEqual(flavor5, 'valid'); + }); + + it('should extract first word from non-conventional PR titles', () => { + // Non-conventional titles starting with action words + const flavor1 = extractPRFlavor('Fix memory leak in authentication', null); + assert.strictEqual(flavor1, 'fix'); + + const flavor2 = extractPRFlavor('Add support for new API endpoint', null); + assert.strictEqual(flavor2, 'add'); + + const flavor3 = extractPRFlavor('Update dependencies to latest versions', null); + assert.strictEqual(flavor3, 'update'); + + const flavor4 = extractPRFlavor('Remove deprecated configuration options', null); + assert.strictEqual(flavor4, 'remove'); + + const flavor5 = extractPRFlavor('Bump version to 2.0.0', null); + assert.strictEqual(flavor5, 'bump'); + + // Should still prefer conventional format over first word + const flavor6 = extractPRFlavor('chore: Update dependencies to latest versions', null); + assert.strictEqual(flavor6, 'chore'); + + // Handle extra whitespace + const flavor7 = extractPRFlavor(' Fix memory leak ', null); + assert.strictEqual(flavor7, 'fix'); + }); + }); + + + describe('FLAVOR_CONFIG integrity', () => { + it('should have unique labels across all configs', () => { + const allLabels = []; + FLAVOR_CONFIG.forEach(config => { + config.labels.forEach(label => { + assert.ok(!allLabels.includes(label), `Duplicate label found: ${label}`); + allLabels.push(label); + }); + }); + }); + + it('should have proper structure for all configs', () => { + FLAVOR_CONFIG.forEach((config, index) => { + assert.ok(Array.isArray(config.labels), `Config ${index} should have labels array`); + assert.ok(config.labels.length > 0, `Config ${index} should have at least one label`); + assert.ok(config.hasOwnProperty('changelog'), `Config ${index} should have changelog property`); + + // changelog should be either a string or undefined + if (config.changelog !== undefined) { + assert.strictEqual(typeof config.changelog, 'string', `Config ${index} changelog should be string or undefined`); + } + + // isFeature should be true or undefined (not false) + if (config.hasOwnProperty('isFeature')) { + assert.strictEqual(config.isFeature, true, `Config ${index} isFeature should be true or undefined`); + } + }); + }); + + it('should have only Features configs with isFeature true', () => { + FLAVOR_CONFIG.forEach(config => { + if (config.isFeature === true) { + assert.strictEqual(config.changelog, 'Features', 'Only Features configs should have isFeature true'); + } + }); + }); + }); +}); \ No newline at end of file diff --git a/danger/dangerfile.js b/danger/dangerfile.js index 6855f24..997a9c0 100644 --- a/danger/dangerfile.js +++ b/danger/dangerfile.js @@ -1,3 +1,5 @@ +const { getFlavorConfig, extractPRFlavor } = require('./dangerfile-utils.js'); + const headRepoName = danger.github.pr.head.repo.git_url; const baseRepoName = danger.github.pr.base.repo.git_url; const isFork = headRepoName != baseRepoName; @@ -36,27 +38,15 @@ if (isFork) { // e.g. "feat" if PR title is "Feat : add more useful stuff" // or "ci" if PR branch is "ci/update-danger" -const prFlavor = (function () { - if (danger.github && danger.github.pr) { - if (danger.github.pr.title) { - const parts = danger.github.pr.title.split(":"); - if (parts.length > 1) { - return parts[0].toLowerCase().trim(); - } - } - if (danger.github.pr.head && danger.github.pr.head.ref) { - const parts = danger.github.pr.head.ref.split("/"); - if (parts.length > 1) { - return parts[0].toLowerCase(); - } - } - } - return ""; -})(); +const prFlavor = extractPRFlavor( + danger.github?.pr?.title, + danger.github?.pr?.head?.ref +); console.log(`::debug:: PR Flavor: '${prFlavor}'`); async function checkDocs() { - if (prFlavor.startsWith("feat")) { + const flavorConfig = getFlavorConfig(prFlavor); + if (flavorConfig.isFeature) { message( 'Do not forget to update Sentry-docs with your feature once the pull request gets approved.' ); @@ -65,10 +55,11 @@ async function checkDocs() { async function checkChangelog() { const changelogFile = "CHANGELOG.md"; + const flavorConfig = getFlavorConfig(prFlavor); - // Check if skipped + // Check if skipped - either by flavor config, explicit skip, or skip label if ( - ["ci", "test", "deps", "chore(deps)", "build(deps)"].includes(prFlavor) || + flavorConfig.changelog === undefined || (danger.github.pr.body + "").includes("#skip-changelog") || (danger.github.pr.labels || []).some(label => label.name === 'skip-changelog') ) { @@ -103,6 +94,7 @@ async function checkChangelog() { } } + /// Report missing changelog entry function reportMissingChangelog(changelogFile) { fail("Please consider adding a changelog entry for the next release.", changelogFile); @@ -113,6 +105,10 @@ function reportMissingChangelog(changelogFile) { .trim() .replace(/\.+$/, ""); + // Determine the appropriate section based on PR flavor + const flavorConfig = getFlavorConfig(prFlavor); + const sectionName = flavorConfig.changelog || "Features"; + markdown( ` ### Instructions and example for changelog @@ -124,6 +120,8 @@ Example: \`\`\`markdown ## Unreleased +### ${sectionName} + - ${prTitleFormatted} ([#${danger.github.pr.number}](${danger.github.pr.html_url})) \`\`\` diff --git a/sentry-cli/integration-test/sentry-server.py b/sentry-cli/integration-test/sentry-server.py index 2e418ac..58d8dce 100644 --- a/sentry-cli/integration-test/sentry-server.py +++ b/sentry-cli/integration-test/sentry-server.py @@ -93,6 +93,8 @@ def do_POST(self): self.writeJSON('{ }') elif self.isApi('api/0/organizations/{}/chunk-upload/'.format(apiOrg)): self.writeJSON('{ }') + elif self.isApi('/api/0/projects/{}/{}/files/proguard-artifact-releases/'.format(apiOrg, apiProject)): + self.writeJSON('{ }') elif self.isApi('api/0/envelope'): sys.stdout.write(" envelope start\n") sys.stdout.write(self.body) diff --git a/sentry-cli/integration-test/tests/action.Tests.ps1 b/sentry-cli/integration-test/tests/action.Tests.ps1 index 332c0da..0c674bf 100644 --- a/sentry-cli/integration-test/tests/action.Tests.ps1 +++ b/sentry-cli/integration-test/tests/action.Tests.ps1 @@ -47,6 +47,7 @@ Describe 'Invoke-SentryServer' { $result = Invoke-SentryServer { Param([string]$url) Invoke-WebRequest -Uri "$url/api/0/projects/org/project/files/dsyms/associate/" -Method Post + Invoke-WebRequest -Uri "$url/api/0/projects/org/project/files/proguard-artifact-releases" -Method Post } Should -ActualValue $result.HasErrors() -BeFalse } diff --git a/updater/README.md b/updater/README.md new file mode 100644 index 0000000..a25f4e5 --- /dev/null +++ b/updater/README.md @@ -0,0 +1,204 @@ +# Updater Composite Action + +Dependency updater - updates dependencies to the latest published git tag and creates/updates PRs. + +## Usage + +```yaml +name: Update Dependencies +on: + # Run every day. + schedule: + - cron: '0 3 * * *' + # And on every PR merge so we get the updated dependencies ASAP, and to make sure the changelog doesn't conflict. + push: + branches: + - main + +permissions: + contents: write # To modify files and create commits + pull-requests: write # To create and update pull requests + actions: write # To cancel previous workflow runs + +jobs: + # Update a git submodule + cocoa: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: modules/sentry-cocoa + name: Cocoa SDK + pattern: '^1\.' # Limit to major version '1' + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update to stable releases only by filtering GitHub release titles + cocoa-stable: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: modules/sentry-cocoa + name: Cocoa SDK (Stable) + gh-title-pattern: '\(Stable\)$' # Only releases with "(Stable)" suffix + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update a properties file + cli: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: sentry-cli.properties + name: CLI + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update using a custom shell script, see updater/scripts/update-dependency.ps1 for the required arguments + agp: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: script.ps1 + name: Gradle Plugin + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update a CMake FetchContent dependency with auto-detection (single dependency only) + sentry-native: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: vendor/sentry-native.cmake + name: Sentry Native SDK + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update a CMake FetchContent dependency with explicit dependency name + deps: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: vendor/dependencies.cmake#googletest + name: GoogleTest + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Update dependencies on a non-default branch (e.g., alpha, beta, or version branches) + # Note: due to limitations in GitHub Actions' schedule trigger, this code needs to be pushed to the default branch. + cocoa-v7: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: modules/sentry-cocoa + name: Cocoa SDK + target-branch: v7 + pattern: '^1\.' # Limit to major version '1' + api-token: ${{ secrets.CI_DEPLOY_KEY }} + + # Use a post-update script (sh or ps1) to make additional changes after dependency update + # The script receives two arguments: original version and new version + post-update-script: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: modules/sentry-cocoa + name: Cocoa SDK + post-update-script: scripts/post-update.sh # Receives args: $1=old version, $2=new version + api-token: ${{ secrets.CI_DEPLOY_KEY }} +``` + +## Inputs + +* `path`: Dependency path in the source repository. Supported formats: + * Submodule path + * Properties file (`.properties`) + * Shell script (`.ps1`, `.sh`) + * CMake file with FetchContent: + * `path/to/file.cmake#DepName` - specify dependency name + * `path/to/file.cmake` - auto-detection (single dependency only) + * type: string + * required: true +* `name`: Name used in the PR title and the changelog entry. + * type: string + * required: true +* `pattern`: RegEx pattern that will be matched against available versions when picking the latest one. + * type: string + * required: false + * default: '' +* `gh-title-pattern`: RegEx pattern to match against GitHub release titles. Only releases with matching titles will be considered. Useful for filtering to specific release channels (e.g., stable releases). + * type: string + * required: false + * default: '' +* `changelog-entry`: Whether to add a changelog entry for the update. + * type: boolean + * required: false + * default: true +* `changelog-section`: Section header to attach the changelog entry to. + * type: string + * required: false + * default: Dependencies +* `pr-strategy`: How to handle PRs. + Can be either of the following: + * `create` - create a new PR for new dependency versions as they are released - maintainers may merge or close older PRs manually + * `update` (default) - keep a single PR that gets updated with new dependency versions until merged - only the latest version update is available at any time +* `target-branch`: Branch to use as base for dependency updates. Defaults to repository default branch if not specified. + * type: string + * required: false + * default: '' (uses repository default branch) +* `post-update-script`: Optional script to run after successful dependency update. Can be a bash script (`.sh`) or PowerShell script (`.ps1`). The script will be executed in the repository root directory before PR creation. The script receives two arguments: + * `$1` / `$args[0]` - The original version (version before update) + * `$2` / `$args[1]` - The new version (version after update) + * type: string + * required: false + * default: '' +* `api-token`: Token for the repo. Can be passed in using `${{ secrets.GITHUB_TOKEN }}`. + If you provide the usual `${{ github.token }}`, no followup CI will run on the created PR. + If you want CI to run on the PRs created by the Updater, you need to provide custom user-specific auth token. + * type: string + * required: true + +### Post-Update Script Example + +**Bash script** (`scripts/post-update.sh`): + +```bash +#!/usr/bin/env bash +set -euo pipefail + +ORIGINAL_VERSION="$1" +NEW_VERSION="$2" + +echo "Updated from $ORIGINAL_VERSION to $NEW_VERSION" +# Make additional changes to repository files here +``` + +**PowerShell script** (`scripts/post-update.ps1`): + +```powershell +param( + [Parameter(Mandatory = $true)][string] $OriginalVersion, + [Parameter(Mandatory = $true)][string] $NewVersion +) + +Write-Output "Updated from $OriginalVersion to $NewVersion" +# Make additional changes to repository files here +``` + +## Outputs + +* `prUrl`: The created/updated PR's URL. +* `baseBranch`: The base branch name. +* `prBranch`: The created/updated PR branch name. +* `originalTag`: The original tag from which the dependency was updated from. +* `latestTag`: The latest tag to which the dependency was updated to. + +## Migration from v2 Reusable Workflow + +If you're migrating from the v2 reusable workflow, see the [changelog migration guide](../CHANGELOG.md#unreleased) for detailed examples. + +Key changes: +- Add `runs-on` to specify the runner +- Move `secrets.api-token` to `with.api-token` +- No need for explicit `actions/checkout` step (handled internally) diff --git a/updater/action.yml b/updater/action.yml new file mode 100644 index 0000000..1d4b3c5 --- /dev/null +++ b/updater/action.yml @@ -0,0 +1,454 @@ +name: 'Dependency Updater' +description: 'Updates dependencies to the latest published tag and creates/updates PRs' +author: 'Sentry' + +inputs: + path: + description: 'Dependency path in the source repository, this can be either a submodule, a .properties file, a shell script, or a CMake file with FetchContent.' + required: true + name: + description: 'Name used in the PR title and the changelog entry.' + required: true + pattern: + description: 'RegEx pattern that will be matched against available versions when picking the latest one.' + required: false + default: '' + gh-title-pattern: + description: 'RegEx pattern to match against GitHub release titles. Only releases with matching titles will be considered.' + required: false + default: '' + changelog-entry: + description: 'Whether to add a changelog entry for the update.' + required: false + default: 'true' + changelog-section: + description: 'Section header to attach the changelog entry to.' + required: false + default: 'Dependencies' + pr-strategy: + description: 'How to handle PRs - can be either "create" (create new PRs for each version) or "update" (keep single PR updated with latest version)' + required: false + default: 'update' + target-branch: + description: 'Branch to use as base for dependency updates. Defaults to repository default branch if not specified.' + required: false + default: '' + api-token: + description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}. Not required if ssh-key is provided, but can be used together with ssh-key for GitHub API operations.' + required: false + default: '' + ssh-key: + description: 'SSH private key for repository authentication. Can be used alone or together with api-token (SSH for git, token for GitHub API).' + required: false + default: '' + post-update-script: + description: 'Optional script to run after successful dependency update. Can be a bash script (.sh) or PowerShell script (.ps1). The script will be executed in the caller-repo directory before PR creation.' + required: false + default: '' + +outputs: + prUrl: + description: 'The created/updated PRs url.' + value: ${{ steps.pr.outputs.url }} + baseBranch: + description: 'The base branch name.' + value: ${{ steps.root.outputs.baseBranch }} + prBranch: + description: 'The created/updated pr branch name.' + value: ${{ steps.root.outputs.prBranch }} + originalTag: + description: 'The original tag from which the dependency was updated from.' + value: ${{ steps.target.outputs.originalTag }} + latestTag: + description: 'The latest tag to which the dependency was updated to.' + value: ${{ steps.target.outputs.latestTag }} + +runs: + using: 'composite' + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@85880fa0301c86cca9da44039ee3bb12d3bedbfa # Tag: 0.12.1 + with: + access_token: ${{ github.token }} + + - name: Validate dependency name + shell: pwsh + run: | + # Validate that inputs.name contains only safe characters + if ('${{ inputs.name }}' -notmatch '^[a-zA-Z0-9_\./@\s-]+$') { + Write-Output "::error::Invalid dependency name: '${{ inputs.name }}'. Only alphanumeric characters, spaces, and _-./@ are allowed." + exit 1 + } + Write-Output "✓ Dependency name '${{ inputs.name }}' is valid" + + - name: Validate dependency path + shell: pwsh + run: | + # Validate that inputs.path contains only safe characters (including # for CMake dependencies) + if ('${{ inputs.path }}' -notmatch '^[a-zA-Z0-9_\./#-]+$') { + Write-Output "::error::Invalid dependency path: '${{ inputs.path }}'. Only alphanumeric characters and _-./# are allowed." + exit 1 + } + Write-Output "✓ Dependency path '${{ inputs.path }}' is valid" + + - name: Validate changelog-entry + shell: pwsh + run: | + # Validate that inputs.changelog-entry is either 'true' or 'false' + if ('${{ inputs.changelog-entry }}' -notin @('true', 'false')) { + Write-Output "::error::Invalid changelog-entry value: '${{ inputs.changelog-entry }}'. Only 'true' or 'false' are allowed." + exit 1 + } + Write-Output "✓ Changelog-entry value '${{ inputs.changelog-entry }}' is valid" + + - name: Validate pr-strategy + shell: pwsh + run: | + # Validate that inputs.pr-strategy is either 'create' or 'update' + if ('${{ inputs.pr-strategy }}' -notin @('create', 'update')) { + Write-Output "::error::Invalid pr-strategy value: '${{ inputs.pr-strategy }}'. Only 'create' or 'update' are allowed." + exit 1 + } + Write-Output "✓ PR strategy value '${{ inputs.pr-strategy }}' is valid" + + - name: Validate post-update-script + if: ${{ inputs.post-update-script != '' }} + shell: pwsh + run: | + # Validate that inputs.post-update-script contains only safe characters + if ('${{ inputs.post-update-script }}' -notmatch '^[a-zA-Z0-9_\./#\s-]+$') { + Write-Output "::error::Invalid post-update-script path: '${{ inputs.post-update-script }}'. Only alphanumeric characters, spaces, and _-./# are allowed." + exit 1 + } + Write-Output "✓ Post-update script path '${{ inputs.post-update-script }}' is valid" + + - name: Validate authentication inputs + shell: pwsh + env: + GH_TOKEN: ${{ inputs.api-token || github.token }} + SSH_KEY: ${{ inputs.ssh-key }} + run: | + $hasToken = -not [string]::IsNullOrEmpty($env:GH_TOKEN) + $hasSshKey = -not [string]::IsNullOrEmpty($env:SSH_KEY) + + if (-not $hasToken -and -not $hasSshKey) { + Write-Output "::error::Either api-token or ssh-key must be provided for authentication." + exit 1 + } + + if ($hasToken -and $hasSshKey) { + Write-Output "✓ Using both SSH key (for git) and token (for GitHub API)" + } elseif ($hasToken) { + Write-Output "✓ Using token authentication" + } else { + Write-Output "✓ Using SSH key authentication" + } + + - name: Validate API token + if: ${{ inputs.api-token != '' }} + shell: pwsh + env: + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: | + # Check if token is actually an SSH key + if ($env:GH_TOKEN -match '-----BEGIN') { + Write-Output "::error::The api-token input appears to contain an SSH private key." + Write-Output "::error::Please use the ssh-key input for SSH authentication instead of api-token." + exit 1 + } + + # Check for whitespace + if ($env:GH_TOKEN -match '\s') { + $tokenLength = $env:GH_TOKEN.Length + $whitespaceMatch = [regex]::Match($env:GH_TOKEN, '\s') + $position = $whitespaceMatch.Index + $char = $whitespaceMatch.Value + $charName = switch ($char) { + "`n" { "newline (LF)" } + "`r" { "carriage return (CR)" } + "`t" { "tab" } + " " { "space" } + default { "whitespace character (code: $([int][char]$char))" } + } + Write-Output "::error::GitHub token contains whitespace at position $position of $tokenLength characters: $charName" + Write-Output "::error::This suggests the token secret may be malformed. Check for extra newlines when setting the secret." + exit 1 + } + + # Check token scopes (works for classic PATs only) + $headers = curl -sS -I -H "Authorization: token $env:GH_TOKEN" https://api.github.com 2>&1 + $scopeLine = $headers | Select-String -Pattern '^x-oauth-scopes:' -CaseSensitive:$false + if ($scopeLine) { + $scopes = $scopeLine -replace '^x-oauth-scopes:\s*', '' -replace '\r', '' + if ([string]::IsNullOrWhiteSpace($scopes)) { + Write-Output "::warning::Token has no scopes. If using a fine-grained PAT, ensure it has Contents (write) and Pull Requests (write) permissions." + } else { + Write-Output "Token scopes: $scopes" + if ($scopes -notmatch '\brepo\b' -and $scopes -notmatch '\bpublic_repo\b') { + Write-Output "::warning::Token may be missing 'repo' or 'public_repo' scope. This may cause issues with private repositories." + } + } + } else { + Write-Output "::notice::Could not detect token scopes (this is normal for fine-grained PATs). Ensure token has Contents (write) and Pull Requests (write) permissions." + } + + # Check token validity and access + gh api repos/${{ github.repository }} --silent 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Output "::error::GitHub token validation failed. Please verify:" + Write-Output " 1. Token is not empty or malformed" + Write-Output " 2. Token has not expired" + Write-Output " 3. Token has an expiration date set" + Write-Output " 4. Token has 'repo' and 'workflow' scopes" + exit 1 + } + + Write-Output "✓ GitHub token is valid and has access to this repository" + + - name: Validate SSH key + if: ${{ inputs.ssh-key != '' }} + shell: pwsh + env: + SSH_KEY: ${{ inputs.ssh-key }} + run: | + # Check if SSH key looks valid + if ($env:SSH_KEY -notmatch '-----BEGIN') { + Write-Output "::warning::SSH key does not appear to start with a PEM header (-----BEGIN). Please verify the key format." + } + + # Check for common SSH key types + $validKeyTypes = @('RSA', 'OPENSSH', 'DSA', 'EC', 'PRIVATE KEY') + $hasValidType = $false + foreach ($type in $validKeyTypes) { + if ($env:SSH_KEY -match "-----BEGIN.*$type") { + $hasValidType = $true + break + } + } + + if (-not $hasValidType) { + Write-Output "::warning::SSH key type not recognized. Supported types: RSA, OPENSSH, DSA, EC, PRIVATE KEY" + } + + Write-Output "✓ SSH key format appears valid" + + # What we need to accomplish: + # * update to the latest tag + # * create a PR + # * update changelog (including the link to the just created PR) + # + # What we actually do is based on whether a PR exists already: + # * YES it does: + # * make the update + # * update changelog (with the ID of an existing PR) + # * push to the PR + # * NO it doesn't: + # * make the update + # * push to a new PR + # * update changelog (with the ID of the just created PR) + # * push to the PR + # We do different approach on subsequent runs because otherwise we would spam users' mailboxes + # with notifications about pushes to existing PRs. This way there is actually no push if not needed. + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ inputs.api-token || github.token }} + ssh-key: ${{ inputs.ssh-key }} + ref: ${{ inputs.target-branch || github.ref }} + path: caller-repo + + - name: Update to the latest version + id: target + shell: pwsh + working-directory: caller-repo + env: + DEPENDENCY_PATH: ${{ inputs.path }} + DEPENDENCY_PATTERN: ${{ inputs.pattern }} + GH_TITLE_PATTERN: ${{ inputs.gh-title-pattern }} + POST_UPDATE_SCRIPT: ${{ inputs.post-update-script }} + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: ${{ github.action_path }}/scripts/update-dependency.ps1 -Path $env:DEPENDENCY_PATH -Pattern $env:DEPENDENCY_PATTERN -GhTitlePattern $env:GH_TITLE_PATTERN -PostUpdateScript $env:POST_UPDATE_SCRIPT + + - name: Get the base repo info + if: steps.target.outputs.latestTag != steps.target.outputs.originalTag + id: root + shell: pwsh + working-directory: caller-repo + env: + PR_STRATEGY: ${{ inputs.pr-strategy }} + DEPENDENCY_PATH: ${{ inputs.path }} + TARGET_BRANCH: ${{ inputs.target-branch }} + run: | + if ([string]::IsNullOrEmpty($env:TARGET_BRANCH)) { + $mainBranch = $(git remote show origin | Select-String "HEAD branch: (.*)").Matches[0].Groups[1].Value + $prBranchPrefix = '' + } else { + $mainBranch = $env:TARGET_BRANCH + $prBranchPrefix = "$mainBranch-" + } + $prBranch = switch ($env:PR_STRATEGY) + { + 'create' { "deps/$env:DEPENDENCY_PATH/${{ steps.target.outputs.latestTag }}" } + 'update' { "deps/$env:DEPENDENCY_PATH" } + default { throw "Unkown PR strategy '$env:PR_STRATEGY'." } + } + $prBranch = $prBranchPrefix + $prBranch + "baseBranch=$mainBranch" | Tee-Object $env:GITHUB_OUTPUT -Append + "prBranch=$prBranch" | Tee-Object $env:GITHUB_OUTPUT -Append + $nonBotCommits = ${{ github.action_path }}/scripts/nonbot-commits.ps1 ` + -RepoUrl "$(git config --get remote.origin.url)" -PrBranch $prBranch -MainBranch $mainBranch + $changed = $nonBotCommits.Length -gt 0 ? 'true' : 'false' + "changed=$changed" | Tee-Object $env:GITHUB_OUTPUT -Append + if ("$changed" -eq "true") + { + Write-Output "::warning::Target branch '$prBranch' has been changed manually - skipping updater to avoid overwriting these changes." + } + + - name: Parse the existing PR URL + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + id: existing-pr + shell: pwsh + working-directory: caller-repo + env: + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: | + $urls = @(gh api 'repos/${{ github.repository }}/pulls?base=${{ steps.root.outputs.baseBranch }}&head=${{ github.repository_owner }}:${{ steps.root.outputs.prBranch }}' --jq '.[].html_url') + if ($urls.Length -eq 0) + { + "url=" | Tee-Object $env:GITHUB_OUTPUT -Append + } + elseif ($urls.Length -eq 1) + { + "url=$($urls[0])" | Tee-Object $env:GITHUB_OUTPUT -Append + } + else + { + throw "Unexpected number of PRs matched ($($urls.Length)): $urls" + } + + - name: Show git diff + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} + shell: bash + working-directory: caller-repo + run: git --no-pager diff + + - name: Get target changelog + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + shell: pwsh + working-directory: caller-repo + env: + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: | + $changelog = ${{ github.action_path }}/scripts/get-changelog.ps1 ` + -RepoUrl '${{ steps.target.outputs.url }}' ` + -OldTag '${{ steps.target.outputs.originalTag }}' ` + -NewTag '${{ steps.target.outputs.latestTag }}' + ${{ github.action_path }}/scripts/set-github-env.ps1 TARGET_CHANGELOG $changelog + + # First we create a PR only if it doesn't exist. We will later overwrite the content with the same action. + - name: Create a PR + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} + uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # pin#v6.0.1 + id: create-pr + env: + DEPENDENCY_PATH: ${{ inputs.path }} + DEPENDENCY_NAME: ${{ inputs.name }} + with: + path: caller-repo + base: ${{ steps.root.outputs.baseBranch }} + branch: ${{ steps.root.outputs.prBranch }} + commit-message: 'chore: update ${{ env.DEPENDENCY_PATH }} to ${{ steps.target.outputs.latestTag }}' + author: 'GitHub ' + title: 'chore(deps): update ${{ env.DEPENDENCY_NAME }} to ${{ steps.target.outputs.latestTagNice }}' + body: | + Bumps ${{ env.DEPENDENCY_PATH }} from ${{ steps.target.outputs.originalTag }} to ${{ steps.target.outputs.latestTag }}. + + Auto-generated by a [dependency updater](https://github.com/getsentry/github-workflows/blob/main/updater/action.yml). + ${{ env.TARGET_CHANGELOG }} + labels: dependencies + + - name: Verify we have a PR + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + id: pr + shell: pwsh + working-directory: caller-repo + run: | + if ('${{ steps.create-pr.outputs.pull-request-url }}' -ne '') + { + "url=${{ steps.create-pr.outputs.pull-request-url }}" | Tee-Object $env:GITHUB_OUTPUT -Append + } + elseif ('${{ steps.existing-pr.outputs.url }}' -ne '') + { + "url=${{ steps.existing-pr.outputs.url }}" | Tee-Object $env:GITHUB_OUTPUT -Append + } + else + { + throw "PR hasn't been created" + } + + # If we had to create a new PR, we must do a clean checkout & update the submodule again. + # If we didn't do this, the new PR would only have a changelog... + - name: 'After new PR: restore repo' + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} + uses: actions/checkout@v4 + with: + token: ${{ inputs.api-token || github.token }} + ssh-key: ${{ inputs.ssh-key }} + ref: ${{ inputs.target-branch || github.ref }} + path: caller-repo + + - name: 'After new PR: redo the update' + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.existing-pr.outputs.url == '') && ( steps.root.outputs.changed == 'false') }} + shell: pwsh + working-directory: caller-repo + env: + DEPENDENCY_PATH: ${{ inputs.path }} + POST_UPDATE_SCRIPT: ${{ inputs.post-update-script }} + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: ${{ github.action_path }}/scripts/update-dependency.ps1 -Path $env:DEPENDENCY_PATH -Tag '${{ steps.target.outputs.latestTag }}' -OriginalTag '${{ steps.target.outputs.originalTag }}' -PostUpdateScript $env:POST_UPDATE_SCRIPT + + - name: Update Changelog + if: ${{ inputs.changelog-entry == 'true' && ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + shell: pwsh + working-directory: caller-repo + env: + DEPENDENCY_NAME: ${{ inputs.name }} + CHANGELOG_SECTION: ${{ inputs.changelog-section }} + GH_TOKEN: ${{ inputs.api-token || github.token }} + run: | + ${{ github.action_path }}/scripts/update-changelog.ps1 ` + -Name $env:DEPENDENCY_NAME ` + -PR '${{ steps.pr.outputs.url }}' ` + -RepoUrl '${{ steps.target.outputs.url }}' ` + -MainBranch '${{ steps.target.outputs.mainBranch }}' ` + -OldTag '${{ steps.target.outputs.originalTag }}' ` + -NewTag '${{ steps.target.outputs.latestTag }}' ` + -Section $env:CHANGELOG_SECTION + + - name: Show final git diff + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + shell: bash + working-directory: caller-repo + run: git --no-pager diff + + # Now make the PR in its final state. This way we only have one commit and no updates if there are no changes between runs. + - name: Update the PR + if: ${{ ( steps.target.outputs.latestTag != steps.target.outputs.originalTag ) && ( steps.root.outputs.changed == 'false') }} + uses: peter-evans/create-pull-request@a4f52f8033a6168103c2538976c07b467e8163bc # pin#v6.0.1 + id: update + env: + DEPENDENCY_PATH: ${{ inputs.path }} + DEPENDENCY_NAME: ${{ inputs.name }} + with: + path: caller-repo + base: ${{ steps.root.outputs.baseBranch }} + branch: ${{ steps.root.outputs.prBranch }} + commit-message: 'chore: update ${{ env.DEPENDENCY_PATH }} to ${{ steps.target.outputs.latestTag }}' + author: 'GitHub ' + title: 'chore(deps): update ${{ env.DEPENDENCY_NAME }} to ${{ steps.target.outputs.latestTagNice }}' + body: | + Bumps ${{ env.DEPENDENCY_PATH }} from ${{ steps.target.outputs.originalTag }} to ${{ steps.target.outputs.latestTag }}. + + Auto-generated by a [dependency updater](https://github.com/getsentry/github-workflows/blob/main/updater/action.yml). + ${{ env.TARGET_CHANGELOG }} + labels: dependencies diff --git a/updater/scripts/cmake-functions.ps1 b/updater/scripts/cmake-functions.ps1 new file mode 100644 index 0000000..8089e90 --- /dev/null +++ b/updater/scripts/cmake-functions.ps1 @@ -0,0 +1,187 @@ +# CMake FetchContent helper functions for update-dependency.ps1 + +function Parse-CMakeFetchContent { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path $_ -PathType Leaf})] + [string]$filePath, + + [Parameter(Mandatory=$false)] + [ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})] + [string]$depName + ) + $content = Get-Content $filePath -Raw + + if ($depName) { + $pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)" + } else { + # Find all FetchContent_Declare blocks + $allMatches = [regex]::Matches($content, "FetchContent_Declare\s*\(\s*([a-zA-Z0-9_-]+)", 'Singleline') + if ($allMatches.Count -eq 1) { + $depName = $allMatches[0].Groups[1].Value + $pattern = "FetchContent_Declare\s*\(\s*$depName\s+([^)]+)\)" + } else { + throw "Multiple FetchContent declarations found. Use #DepName syntax." + } + } + + $match = [regex]::Match($content, $pattern, 'Singleline,IgnoreCase') + if (-not $match.Success) { + throw "FetchContent_Declare for '$depName' not found in $filePath" + } + $block = $match.Groups[1].Value + + # Look for GIT_REPOSITORY and GIT_TAG patterns specifically + # Exclude matches that are in comments (lines starting with #) + $repoMatch = [regex]::Match($block, '(?m)^\s*GIT_REPOSITORY\s+(\S+)') + $tagMatch = [regex]::Match($block, '(?m)^\s*GIT_TAG\s+(\S+)') + + $repo = if ($repoMatch.Success) { $repoMatch.Groups[1].Value } else { "" } + $tag = if ($tagMatch.Success) { $tagMatch.Groups[1].Value } else { "" } + + if ([string]::IsNullOrEmpty($repo) -or [string]::IsNullOrEmpty($tag)) { + throw "Could not parse GIT_REPOSITORY or GIT_TAG from FetchContent_Declare block" + } + + return @{ GitRepository = $repo; GitTag = $tag; DepName = $depName } +} + +function Find-TagForHash { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$repo, + + [Parameter(Mandatory=$true)] + [ValidatePattern('^[a-f0-9]{40}$')] + [string]$hash + ) + try { + $refs = git ls-remote --tags $repo + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch tags from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)" + } + foreach ($ref in $refs) { + $commit, $tagRef = $ref -split '\s+', 2 + if ($commit -eq $hash) { + return $tagRef -replace '^refs/tags/', '' + } + } + return $null + } + catch { + Write-Host "Warning: Could not resolve hash $hash to tag name: $_" + return $null + } +} + +function Test-HashAncestry { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$repo, + + [Parameter(Mandatory=$true)] + [ValidatePattern('^[a-f0-9]{40}$')] + [string]$oldHash, + + [Parameter(Mandatory=$true)] + [ValidatePattern('^[a-f0-9]{40}$')] + [string]$newHash + ) + try { + # Create a temporary directory for git operations + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid()) + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + try { + Push-Location $tempDir + + # Initialize a bare repository and add the remote + git init --bare 2>$null | Out-Null + git remote add origin $repo 2>$null | Out-Null + + # Fetch both commits + git fetch origin $oldHash 2>$null | Out-Null + git fetch origin $newHash 2>$null | Out-Null + + # Check if old hash is ancestor of new hash + git merge-base --is-ancestor $oldHash $newHash 2>$null + $isAncestor = $LastExitCode -eq 0 + + return $isAncestor + } + finally { + Pop-Location + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + catch { + Write-Host "Error: Could not validate ancestry for $oldHash -> $newHash : $_" + # When in doubt, fail safely to prevent incorrect updates + return $false + } +} + +function Update-CMakeFile { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)] + [ValidateScript({Test-Path $_ -PathType Leaf})] + [string]$filePath, + + [Parameter(Mandatory=$false)] + [ValidateScript({[string]::IsNullOrEmpty($_) -or $_ -match '^[a-zA-Z][a-zA-Z0-9_.-]*$'})] + [string]$depName, + + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string]$newValue + ) + $content = Get-Content $filePath -Raw + $fetchContent = Parse-CMakeFetchContent $filePath $depName + $originalValue = $fetchContent.GitTag + $repo = $fetchContent.GitRepository + $wasHash = $originalValue -match '^[a-f0-9]{40}$' + + if ($wasHash) { + # Convert tag to hash and add comment + $newHashRefs = git ls-remote $repo "refs/tags/$newValue" + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch tag $newValue from repository $repo (git ls-remote failed with exit code $LASTEXITCODE)" + } + if (-not $newHashRefs) { + throw "Tag $newValue not found in repository $repo" + } + $newHash = ($newHashRefs -split '\s+')[0] + $replacement = "$newHash # $newValue" + + # Validate ancestry: ensure old hash is reachable from new tag + if (-not (Test-HashAncestry $repo $originalValue $newHash)) { + throw "Cannot update: hash $originalValue is not in history of tag $newValue" + } + } else { + $replacement = $newValue + } + + # Update GIT_TAG value, replacing entire line content after GIT_TAG + # This removes potentially outdated version-specific comments + $pattern = "(FetchContent_Declare\s*\(\s*$depName\s+[^)]*GIT_TAG\s+)[^\r\n]+(\r?\n[^)]*\))" + $newContent = [regex]::Replace($content, $pattern, "`${1}$replacement`${2}", 'Singleline') + + if ($newContent -eq $content) { + throw "Failed to update GIT_TAG in $filePath - pattern may not have matched" + } + + $newContent | Out-File $filePath -NoNewline + + # Verify the update worked + $verifyContent = Parse-CMakeFetchContent $filePath $depName + $expectedValue = $wasHash ? $newHash : $newValue + if ($verifyContent.GitTag -notmatch [regex]::Escape($expectedValue)) { + throw "Update verification failed - read-after-write did not match expected value" + } +} diff --git a/updater/scripts/get-changelog.ps1 b/updater/scripts/get-changelog.ps1 index 90d5d72..c892160 100644 --- a/updater/scripts/get-changelog.ps1 +++ b/updater/scripts/get-changelog.ps1 @@ -5,122 +5,267 @@ param( ) Set-StrictMode -Version latest +$PSNativeCommandErrorActionPreference = $false +$ErrorActionPreference = 'Stop' $prefix = 'https?://(www\.)?github.com/' -if (-not ($RepoUrl -match "^$prefix")) -{ - Write-Warning "Only github.com repositories are currently supported. Given RepoUrl doesn't look like one: $RepoUrl" +if (-not ($RepoUrl -match "^$prefix([^/]+)/([^/]+?)(?:\.git)?/?$")) { + Write-Warning "Only https://github.com repositories are currently supported. Could not parse repository from URL: $RepoUrl" return } +$repoOwner = $matches[2] +$repoName = $matches[3] +$apiRepo = "$repoOwner/$repoName" + +# Create temporary directory for changelog files $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid()) New-Item -ItemType Directory $tmpDir | Out-Null -try -{ - git clone --depth 1 $RepoUrl $tmpDir +# Function to try different changelog filenames +function Get-ChangelogContent { + param($ref, $filePath) - $file = $(Get-ChildItem -Path $tmpDir | Where-Object { $_.Name -match '^changelog(\.md|\.txt|)$' } ) - if ("$file" -eq '') - { - Write-Warning "Couldn't find a changelog" - return - } - elseif ($file -is [Array]) - { - Write-Warning "Multiple changelogs found: $file" - return + $changelogNames = @('CHANGELOG.md', 'changelog.md', 'CHANGELOG.txt', 'changelog.txt', 'CHANGELOG') + + foreach ($name in $changelogNames) { + try { + # Try fetching directly from raw.githubusercontent.com + $rawUrl = "https://raw.githubusercontent.com/$apiRepo/$ref/$name" + $content = Invoke-RestMethod -Uri $rawUrl -Method Get -ErrorAction SilentlyContinue + if ($content) { + Set-Content -Path $filePath -Value $content -Encoding UTF8 + Write-Host "Found $name for ref $ref" + return $true + } + } catch { + # Continue to next filename + } } - Write-Host "Found changelog: $file" - [string[]]$lines = Get-Content $file -} -finally -{ - Write-Host "Removing $tmpDir" - Remove-Item -Recurse -Force -ErrorAction Continue -Path $tmpDir + return $false } -$startIndex = -1 -$endIndex = -1 -$changelog = '' -for ($i = 0; $i -lt $lines.Count; $i++) -{ - $line = $lines[$i] - - if ($startIndex -lt 0) - { - if ($line -match "^#+ +v?$NewTag\b") - { - $startIndex = $i - } +# Function to generate changelog from git commits +function Get-ChangelogFromCommits { + param($repoUrl, $oldTag, $newTag, $tmpDir) + + # Clone the repository + $repoDir = Join-Path $tmpDir 'repo' + Write-Host "Cloning repository to generate changelog from commits..." + git clone --no-single-branch --quiet $repoUrl $repoDir + if ($LASTEXITCODE -ne 0) { + Write-Warning "Could not clone repository $repoUrl" + return $null } - elseif ($line -match "^#+ +v?$OldTag\b") - { - $endIndex = $i - 1 - break + + if (-not (Test-Path $repoDir)) { + Write-Warning "Repository directory was not created successfully" + return $null } -} -# If the changelog doesn't have a section for the oldTag, stop at the first SemVer that's lower than oldTag. -if ($endIndex -lt 0) -{ - $endIndex = $lines.Count - 1 # fallback, may be overwritten below - try - { - $semverOldTag = [System.Management.Automation.SemanticVersion]::Parse($OldTag) - for ($i = $startIndex; $i -lt $lines.Count; $i++) - { - $line = $lines[$i] - if ($line -match '^#+ +v?([0-9]+.*)$') - { - try - { - if ($semverOldTag -ge [System.Management.Automation.SemanticVersion]::Parse($matches[1])) - { - $endIndex = $i - 1 - break - } - } - catch {} + Push-Location $repoDir + try { + # Ensure we have both tags + git fetch --tags --quiet + if ($LASTEXITCODE -ne 0) { + Write-Warning "Could not fetch tags from repository" + return $null + } + + # Get commit messages between tags + Write-Host "Getting commits between $oldTag and $newTag..." + $commitMessages = git log "$oldTag..$newTag" --pretty=format:'%s' + if ($LASTEXITCODE -ne 0) { + Write-Warning "Could not get commits between $oldTag and $newTag (exit code: $LASTEXITCODE)" + return $null + } + + if ([string]::IsNullOrEmpty($commitMessages)) { + Write-Host "No commits found between $oldTag and $newTag" + return $null + } + + # Filter out version tag commits and format as list + $commits = $commitMessages -split "`n" | + Where-Object { + $_ -and + $_ -notmatch '^\s*v?\d+\.\d+\.\d+' -and # Skip version commits + $_.Trim().Length -gt 0 + } | + ForEach-Object { "- $_" } + + if ($commits.Count -eq 0) { + Write-Host "No meaningful commits found between $oldTag and $newTag" + return $null + } + + # Create changelog from commits + $changelog = "## Changelog`n`n" + $changelog += "### Commits between $oldTag and $newTag`n`n" + $changelog += $commits -join "`n" + + Write-Host "Generated changelog from $($commits.Count) commits" + return $changelog + } + catch { + Write-Warning "Error generating changelog from commits: $($_.Exception.Message)" + return $null + } + finally { + Pop-Location + # Ensure repository directory is cleaned up + if (Test-Path $repoDir) { + try { + Remove-Item -Recurse -Force $repoDir -ErrorAction SilentlyContinue + Write-Host "Cleaned up temporary repository directory" + } + catch { + Write-Warning "Could not clean up temporary repository directory: $repoDir" } } } - catch {} } -# Slice changelog lines from startIndex to endIndex. -if ($startIndex -ge 0) -{ - $changelog = ($lines[$startIndex..$endIndex] -join "`n").Trim() -} -else -{ - $changelog = '' +# Function to generate changelog from diff between changelog files +function Get-ChangelogFromDiff { + param($oldTag, $newTag, $tmpDir) + + # Try to fetch changelog files for both tags + $oldChangelogPath = Join-Path $tmpDir 'old-changelog.md' + $hasOldChangelog = Get-ChangelogContent $oldTag $oldChangelogPath + + $newChangelogPath = Join-Path $tmpDir 'new-changelog.md' + $hasNewChangelog = Get-ChangelogContent $newTag $newChangelogPath + + # Return null if we don't have both changelog files + if (-not $hasOldChangelog -or -not $hasNewChangelog) { + return $null + } + + Write-Host "Generating changelog diff between $oldTag and $newTag..." + + # Generate diff using git diff --no-index + # git diff returns exit code 1 when differences are found, which is expected behavior + $fullDiff = git diff --no-index $oldChangelogPath $newChangelogPath + + # The first lines are diff metadata, skip them + $fullDiff = $fullDiff -split "`n" | Select-Object -Skip 4 + if ([string]::IsNullOrEmpty("$fullDiff")) { + Write-Host "No differences found between $oldTag and $newTag" + return $null + } else { + Write-Host "Successfully created a changelog diff - $($fullDiff.Count) lines" + } + + # Extract only the added lines (lines starting with + but not ++) + $addedLines = $fullDiff | Where-Object { $_ -match '^[+][^+]*' } | ForEach-Object { $_.Substring(1) } + + if ($addedLines.Count -eq 0) { + Write-Host "No changelog additions found between $oldTag and $newTag" + return $null + } + + # Create clean changelog from added lines + $changelog = ($addedLines -join "`n").Trim() + + if ($changelog.Length -eq 0) { + return $null + } + + # Add header if needed + if (-not ($changelog -match '^(##|#) Changelog')) { + $changelog = "## Changelog`n`n$changelog" + } + + # Increase header level by one for content (not the main header) + $changelog = $changelog -replace '(^|\n)(#+) ', '$1$2# ' -replace '^### Changelog', '## Changelog' + + # Only add details section if there are deletions or modifications (not just additions) + $hasModifications = $fullDiff | Where-Object { $_ -match '^[-]' -and $_ -notmatch '^[-]{3}' } + if ($hasModifications) { + $changelog += "`n`n
`nFull CHANGELOG.md diff`n`n" + $changelog += '```diff' + "`n" + $changelog += $fullDiff -join "`n" + $changelog += "`n" + '```' + "`n`n
" + } + + return $changelog } -if ($changelog.Length -gt 1) -{ - $changelog = "# Changelog`n$changelog" - # Increase header level by one. - $changelog = $changelog -replace '(^|\n)(#+) ', '$1$2# ' - # Remove at-mentions. + +# Function to sanitize and format changelog content +function Format-ChangelogContent { + param($changelog, $repoUrl) + + if ([string]::IsNullOrEmpty($changelog)) { + return $null + } + + # Apply standard formatting + # Remove at-mentions $changelog = $changelog -replace '@', '' - # Make PR/issue references into links to the original repository (unless they already are links). - $changelog = $changelog -replace '(? :warning: **Changelog content truncated by $($oldLength - $changelog.Length) characters because it was over the limit ($limit) and wouldn't fit into PR description.**" + } + + Write-Host "Final changelog length: $($changelog.Length) characters" + return $changelog } -# Limit the changelog length to ~60k to allow for other text in the PR body (total PR limit is 65536 characters). -$limit = 60000 -if ($changelog.Length -gt $limit) -{ - $oldLength = $changelog.Length - Write-Warning "Truncating changelog because it's $($changelog.Length - $limit) characters longer than the limit $limit." - while ($changelog.Length -gt $limit) - { - $changelog = $changelog.Substring(0, $changelog.LastIndexOf("`n")) - } - $changelog += "`n`n> :warning: **Changelog content truncated by $($oldLength - $changelog.Length) characters because it was over the limit ($limit) and wouldn't fit into PR description.**" +try { + Write-Host 'Fetching CHANGELOG files for comparison...' + + $changelog = $null + + # Try changelog file diff first, fall back to git commits if not available + $changelog = Get-ChangelogFromDiff $OldTag $NewTag $tmpDir + + # Fall back to git commits if no changelog files or no diff found + if (-not $changelog) { + Write-Host "No changelog files found or no changes detected, falling back to git commits..." + $changelog = Get-ChangelogFromCommits $RepoUrl $OldTag $NewTag $tmpDir + } + + # Apply formatting and output result + if ($changelog) { + $formattedChangelog = Format-ChangelogContent $changelog $RepoUrl + if ($formattedChangelog) { + Write-Output $formattedChangelog + } else { + Write-Host "No changelog content to display after formatting" + } + } else { + Write-Host "No changelog found between $OldTag and $NewTag" + } +} catch { + Write-Warning "Failed to get changelog: $($_.Exception.Message)" +} finally { + if (Test-Path $tmpDir) { + Write-Host 'Cleaning up temporary files...' + Remove-Item -Recurse -Force -ErrorAction Continue $tmpDir + Write-Host 'Cleanup complete.' + } } -$changelog +# This resets the $LASTEXITCODE set by git diff above. +# Note that this only runs in the successful path. +exit 0 diff --git a/updater/scripts/update-changelog.ps1 b/updater/scripts/update-changelog.ps1 index 5c054e1..1ab2ec5 100644 --- a/updater/scripts/update-changelog.ps1 +++ b/updater/scripts/update-changelog.ps1 @@ -97,7 +97,6 @@ for ($i = 0; $i -lt $lines.Count; $i++) throw "Prettier comment format - expected , but found: '$line'" } # End of prettier comment - # Next, we expect a header if (-not $line.StartsWith("#")) @@ -180,9 +179,8 @@ for ($i = 0; $i -lt $sectionEnd; $i++) if (!$updated) { # Find what character is used as a bullet-point separator - look for the first bullet-point object that wasn't created by this script. - $bulletPoint = $lines | Where-Object { ($_ -match "^ *[-*] ") -and -not ($_ -match "(Bump .* to|\[changelog\]|\[diff\])") } | Select-Object -First 1 - $bulletPoint = "$bulletPoint-"[0] - + $bulletPoint = $lines | Where-Object { ($_ -match '^ *[-*] ') -and -not ($_ -match '(Bump .* to|\[changelog\]|\[diff\])') } | Select-Object -First 1 + $bulletPoint = "$("$bulletPoint".Trim())-"[0] $entry = @("$bulletPoint Bump $Name from $oldTagNice to $newTagNice ($PullRequestMD)", " $bulletPoint [changelog]($RepoUrl/blob/$MainBranch/CHANGELOG.md#$tagAnchor)", " $bulletPoint [diff]($RepoUrl/compare/$OldTag...$NewTag)") diff --git a/updater/scripts/update-dependency.ps1 b/updater/scripts/update-dependency.ps1 index c9af07b..dec0b20 100644 --- a/updater/scripts/update-dependency.ps1 +++ b/updater/scripts/update-dependency.ps1 @@ -6,122 +6,151 @@ param( # * `get-version` - return the currently specified dependency version # * `get-repo` - return the repository url (e.g. https://github.com/getsentry/dependency) # * `set-version` - update the dependency version (passed as another string argument after this one) + # - a CMake file (.cmake) with FetchContent_Declare statements: + # * Use `path/to/file.cmake#DepName` to specify dependency name + # * Or just `path/to/file.cmake` if file contains single FetchContent_Declare [Parameter(Mandatory = $true)][string] $Path, # RegEx pattern that will be matched against available versions when picking the latest one [string] $Pattern = '', + # RegEx pattern to match against GitHub release titles. Only releases with matching titles will be considered + [string] $GhTitlePattern = '', # Specific version - if passed, no discovery is performed and the version is set directly - [string] $Tag = '' + [string] $Tag = '', + # Version that the dependency was on before the update - should be only passed if $Tag is set. Necessary for PostUpdateScript. + [string] $OriginalTag = '', + # Optional post-update script to run after successful dependency update + # The script receives the original and new version as arguments + [string] $PostUpdateScript = '' ) +$ErrorActionPreference = 'Stop' Set-StrictMode -Version latest . "$PSScriptRoot/common.ps1" -if (-not (Test-Path $Path )) -{ +# Parse CMake file with dependency name +if ($Path -match '^(.+\.cmake)(#(.+))?$') { + $Path = $Matches[1] # Set Path to file for existing logic + if ($Matches[3]) { + $cmakeDep = $Matches[3] + # Validate dependency name follows CMake naming conventions + if ($cmakeDep -notmatch '^[a-zA-Z][a-zA-Z0-9_.-]*$') { + throw "Invalid CMake dependency name: '$cmakeDep'. Must start with letter and contain only alphanumeric, underscore, dot, or hyphen." + } + } else { + $cmakeDep = $null # Will auto-detect + } + $isCMakeFile = $true +} else { + $cmakeDep = $null + $isCMakeFile = $false +} + +if (-not (Test-Path $Path )) { throw "Dependency $Path doesn't exit"; } # If it's a directory, we consider it a submodule dependendency. Otherwise, it must a properties-style file or a script. $isSubmodule = (Test-Path $Path -PathType Container) -function SetOutput([string] $name, $value) -{ - if (Test-Path env:GITHUB_OUTPUT) - { +function SetOutput([string] $name, $value) { + if (Test-Path env:GITHUB_OUTPUT) { "$name=$value" | Tee-Object $env:GITHUB_OUTPUT -Append - } - else - { + } else { "$name=$value" } } -if (-not $isSubmodule) -{ +if (-not $isSubmodule) { $isScript = $Path -match '\.(ps1|sh)$' - function DependencyConfig ([Parameter(Mandatory = $true)][string] $action, [string] $value = $null) - { - if ($isScript) - { - if (Get-Command 'chmod' -ErrorAction SilentlyContinue) - { + function DependencyConfig ([Parameter(Mandatory = $true)][string] $action, [string] $value = $null) { + if ($isCMakeFile) { + # CMake file handling + switch ($action) { + 'get-version' { + $fetchContent = Parse-CMakeFetchContent $Path $cmakeDep + $currentValue = $fetchContent.GitTag + if ($currentValue -match '^[a-f0-9]{40}$') { + # Try to resolve hash to tag for version comparison + $repo = $fetchContent.GitRepository + $tagForHash = Find-TagForHash $repo $currentValue + return $tagForHash ?? $currentValue + } + return $currentValue + } + 'get-repo' { + return (Parse-CMakeFetchContent $Path $cmakeDep).GitRepository + } + 'set-version' { + Update-CMakeFile $Path $cmakeDep $value + } + default { + throw "Unknown action $action" + } + } + } elseif ($isScript) { + if (Get-Command 'chmod' -ErrorAction SilentlyContinue) { chmod +x $Path - if ($LastExitCode -ne 0) - { + if ($LastExitCode -ne 0) { throw 'chmod failed'; } } - try - { + try { $result = & $Path $action $value $failed = -not $? - } - catch - { + } catch { $result = $_ $failed = $true } - if ($failed) - { + if ($failed) { throw "Script execution failed: $Path $action $value | output: $result" } return $result - } - else - { - switch ($action) - { - 'get-version' - { + } else { + switch ($action) { + 'get-version' { return (Get-Content $Path -Raw | ConvertFrom-StringData).version } - 'get-repo' - { + 'get-repo' { return (Get-Content $Path -Raw | ConvertFrom-StringData).repo } - 'set-version' - { + 'set-version' { $content = Get-Content $Path $content = $content -replace '^(?version *= *).*$', "`${prop}$value" $content | Out-File $Path $readVersion = (Get-Content $Path -Raw | ConvertFrom-StringData).version - if ("$readVersion" -ne "$value") - { + if ("$readVersion" -ne "$value") { throw "Update failed - read-after-write yielded '$readVersion' instead of expected '$value'" } } - Default - { + default { throw "Unknown action $action" } } } } + + # Load CMake helper functions + . "$PSScriptRoot/cmake-functions.ps1" } -if ("$Tag" -eq '') -{ - if ($isSubmodule) - { +if ("$Tag" -eq '') { + $OriginalTag | Should -Be '' + + if ($isSubmodule) { git submodule update --init --no-fetch --single-branch $Path Push-Location $Path - try - { + try { $originalTag = $(git describe --tags) git fetch --tags [string[]]$tags = $(git tag --list) $url = $(git remote get-url origin) $mainBranch = $(git remote show origin | Select-String 'HEAD branch: (.*)').Matches[0].Groups[1].Value - } - finally - { + } finally { Pop-Location } - } - else - { + } else { $originalTag = DependencyConfig 'get-version' $url = DependencyConfig 'get-repo' @@ -130,8 +159,7 @@ if ("$Tag" -eq '') $tags = $tags | ForEach-Object { ($_ -split '\s+')[1] -replace '^refs/tags/', '' } $headRef = ($(git ls-remote $url HEAD) -split '\s+')[0] - if ("$headRef" -eq '') - { + if ("$headRef" -eq '') { throw "Couldn't determine repository head (no ref returned by ls-remote HEAD" } $mainBranch = (git ls-remote --heads $url | Where-Object { $_.StartsWith($headRef) }) -replace '.*\srefs/heads/', '' @@ -139,8 +167,42 @@ if ("$Tag" -eq '') $url = $url -replace '\.git$', '' - if ("$Pattern" -eq '') - { + # Filter by GitHub release titles if pattern is provided + if ("$GhTitlePattern" -ne '') { + Write-Host "Filtering tags by GitHub release title pattern '$GhTitlePattern'" + + # Parse GitHub repo owner/name from URL + if ($url -notmatch 'github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$') { + throw "Could not parse GitHub owner/repo from URL: $url" + } + + $owner, $repo = $Matches[1], $Matches[2] + + # Fetch releases from GitHub API + $releases = @(gh api "repos/$owner/$repo/releases" --paginate --jq '.[] | {tag_name: .tag_name, name: .name}' | ConvertFrom-Json) + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch GitHub releases from $owner/$repo (exit code: $LASTEXITCODE)" + } + + # Find tags that have matching release titles + $validTags = @{} + foreach ($release in $releases) { + if ($release.name -match $GhTitlePattern) { + $validTags[$release.tag_name] = $true + } + } + + # Filter tags to only include those with matching release titles + $originalTagCount = $tags.Length + $tags = @($tags | Where-Object { $validTags.ContainsKey($_) }) + Write-Host "GitHub release title filtering: $originalTagCount -> $($tags.Count) tags" + + if ($tags.Count -eq 0) { + throw "Found no tags with GitHub releases matching title pattern '$GhTitlePattern'" + } + } + + if ("$Pattern" -eq '') { # Use a default pattern that excludes pre-releases $Pattern = '^v?([0-9.]+)$' } @@ -148,8 +210,7 @@ if ("$Tag" -eq '') Write-Host "Filtering tags with pattern '$Pattern'" $tags = $tags -match $Pattern - if ($tags.Length -le 0) - { + if ($tags.Length -le 0) { throw "Found no tags matching pattern '$Pattern'" } @@ -158,14 +219,11 @@ if ("$Tag" -eq '') Write-Host "Sorted tags: $tags" $latestTag = $tags[-1] - if (("$originalTag" -ne '') -and ("$latestTag" -ne '') -and ("$latestTag" -ne "$originalTag")) - { - do - { + if (("$originalTag" -ne '') -and ("$latestTag" -ne '') -and ("$latestTag" -ne "$originalTag")) { + do { # It's possible that the dependency was updated to a pre-release version manually in which case we don't want to # roll back, even though it's not the latest version matching the configured pattern. - if ((GetComparableVersion $originalTag) -ge (GetComparableVersion $latestTag)) - { + if ((GetComparableVersion $originalTag) -ge (GetComparableVersion $latestTag)) { Write-Host "SemVer represented by the original tag '$originalTag' is newer than the latest tag '$latestTag'. Skipping update." $latestTag = $originalTag break @@ -175,8 +233,7 @@ if ("$Tag" -eq '') $refs = $(git ls-remote --tags $url) $refOriginal = (($refs -match "refs/tags/$originalTag" ) -split '[ \t]') | Select-Object -First 1 $refLatest = (($refs -match "refs/tags/$latestTag" ) -split '[ \t]') | Select-Object -First 1 - if ($refOriginal -eq $refLatest) - { + if ($refOriginal -eq $refLatest) { Write-Host "Latest tag '$latestTag' points to the same commit as the original tag '$originalTag'. Skipping update." $latestTag = $originalTag break @@ -192,23 +249,43 @@ if ("$Tag" -eq '') SetOutput 'url' $url SetOutput 'mainBranch' $mainBranch - if ("$originalTag" -eq "$latestTag") - { + if ("$originalTag" -eq "$latestTag") { return } $Tag = $latestTag +} else { + $OriginalTag | Should -Not -Be '' } -if ($isSubmodule) -{ +if ($isSubmodule) { Write-Host "Updating submodule $Path to $Tag" Push-Location $Path git checkout $Tag Pop-Location -} -else -{ +} else { Write-Host "Updating 'version' in $Path to $Tag" DependencyConfig 'set-version' $tag } + +# Run post-update script if provided +if ("$PostUpdateScript" -ne '') { + Write-Host "Running post-update script: $PostUpdateScript" + if (-not (Test-Path $PostUpdateScript)) { + throw "Post-update script not found: $PostUpdateScript" + } + + if (Get-Command 'chmod' -ErrorAction SilentlyContinue) { + chmod +x $PostUpdateScript + if ($LastExitCode -ne 0) { + throw 'chmod failed'; + } + } + + & $PostUpdateScript "$originalTag" "$tag" + if ($LastExitCode -ne 0) { + throw "Post-update script failed with exit code $LastExitCode" + } + + Write-Host '✓ Post-update script completed successfully' +} diff --git a/updater/tests/get-changelog.Tests.ps1 b/updater/tests/get-changelog.Tests.ps1 index 0a09e0d..aaa48c1 100644 --- a/updater/tests/get-changelog.Tests.ps1 +++ b/updater/tests/get-changelog.Tests.ps1 @@ -2,31 +2,15 @@ Describe 'get-changelog' { It 'with existing versions' { $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` - -RepoUrl 'https://github.com/getsentry/github-workflows' -OldTag '1.0.0' -NewTag '2.1.0' + -RepoUrl 'https://github.com/getsentry/github-workflows' -OldTag 'v2.0.0' -NewTag 'v2.1.0' $expected = @' ## Changelog + ### 2.1.0 #### Features - New reusable workflow, `danger.yml`, to check Pull Requests with predefined rules ([#34](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/34)) - -### 2.0.0 - -#### Changes - -- Rename `api_token` secret to `api-token` ([#21](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/21)) -- Change changelog target section header from "Features" to "Dependencies" ([#19](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/19)) - -#### Features - -- Add `pr-strategy` switch to choose between creating new PRs or updating an existing one ([#22](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/22)) -- Add `changelog-section` input setting to specify target changelog section header ([#19](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/19)) - -#### Fixes - -- Preserve changelog bullet-point format ([#20](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/20)) -- Changelog section parsing when an entry text contains the section name in the text ([#25](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/25)) '@ $actual | Should -Be $expected @@ -34,7 +18,7 @@ Describe 'get-changelog' { It 'with missing versions' { $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` - -RepoUrl 'https://github.com/getsentry/sentry-javascript' -OldTag 'XXXXXXX' -NewTag 'YYYYYYYYY' + -RepoUrl 'https://github.com/getsentry/github-workflows' -OldTag 'XXXXXXX' -NewTag 'YYYYYYYYY' $actual | Should -BeNullOrEmpty } @@ -57,6 +41,7 @@ Describe 'get-changelog' { -RepoUrl 'https://github.com/getsentry/sentry-cli' -OldTag '2.1.0' -NewTag '2.2.0' $expected = @' ## Changelog + ### 2.2.0 #### Various fixes & improvements @@ -73,6 +58,7 @@ Describe 'get-changelog' { -RepoUrl 'https://github.com/getsentry/sentry-native' -OldTag '0.4.16' -NewTag '0.4.17' $expected = @' ## Changelog + ### 0.4.17 **Fixes**: @@ -91,15 +77,17 @@ Features, fixes and improvements in this release have been contributed by: It 'Does not show versions older than OldTag even if OldTag is missing' { $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` - -RepoUrl 'https://github.com/getsentry/github-workflows' -OldTag '2.1.5' -NewTag '2.2.1' + -RepoUrl 'https://github.com/getsentry/github-workflows' -OldTag 'v2.1.1' -NewTag 'v2.2.1' $actual | Should -Be @' ## Changelog + ### 2.2.1 #### Fixes - Support comments when parsing pinned actions in Danger ([#40](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/40)) + ### 2.2.0 #### Features @@ -110,7 +98,7 @@ Features, fixes and improvements in this release have been contributed by: It 'truncates too long text' { $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` - -RepoUrl 'https://github.com/getsentry/sentry-cli' -OldTag '1.0.0' -NewTag '2.4.0' + -RepoUrl 'https://github.com/getsentry/sentry-cli' -OldTag '1.60.0' -NewTag '2.32.0' if ($actual.Length -gt 61000) { throw "Expected the content to be truncated to less-than 61k characters, but got: $($actual.Length)" @@ -128,16 +116,269 @@ Features, fixes and improvements in this release have been contributed by: -RepoUrl 'https://github.com/getsentry/sentry-native' -OldTag '0.7.17' -NewTag '0.7.18' $expected = @' ## Changelog + ### 0.7.18 **Features**: - Add support for Xbox Series X/S. ([#1100](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1100)) - Add option to set debug log level. ([#1107](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1107)) -- Add `traces_sampler`. ([#1108](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1108)) +- Add `traces_sampler` ([#1108](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1108)) - Provide support for C++17 compilers when using the `crashpad` backend. ([#1110](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1110), [crashpad#116](https://github-redirect.dependabot.com/getsentry/crashpad/pull/116), [mini_chromium#1](https://github-redirect.dependabot.com/getsentry/mini_chromium/pull/1)) '@ $actual | Should -Be $expected } + + It 'handles commit SHA as OldTag by resolving to tag' { + # Test with a SHA that corresponds to a known tag (0.9.1) + # This should resolve the SHA to the tag and use normal changelog logic + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/sentry-native' ` + -OldTag 'a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2' ` + -NewTag '0.11.0' + + $expected = @' +## Changelog + +### 0.11.0 + +**Breaking changes**: + +- Add `user_data` parameter to `traces_sampler`. ([#1346](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1346)) + +**Fixes**: + +- Include `stddef.h` explicitly in `crashpad` since future `libc++` revisions will stop providing this include transitively. ([#1375](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1375), [crashpad#132](https://github-redirect.dependabot.com/getsentry/crashpad/pull/132)) +- Fall back on `JWASM` in the _MinGW_ `crashpad` build only if _no_ `CMAKE_ASM_MASM_COMPILER` has been defined. ([#1375](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1375), [crashpad#133](https://github-redirect.dependabot.com/getsentry/crashpad/pull/133)) +- Prevent `crashpad` from leaking Objective-C ARC compile options into any parent target linkage. ([#1375](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1375), [crashpad#134](https://github-redirect.dependabot.com/getsentry/crashpad/pull/134)) +- Fixed a TOCTOU race between session init/shutdown and event capture. ([#1377](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1377)) +- Make the Windows resource generation aware of config-specific output paths for multi-config generators. ([#1383](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1383)) +- Remove the `ASM` language from the top-level CMake project, as this triggered CMake policy `CMP194` which isn't applicable to the top-level. ([#1384](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1384)) + +**Features**: + +- Add a configuration to disable logging after a crash has been detected - `sentry_options_set_logger_enabled_when_crashed()`. ([#1371](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1371)) + +**Internal**: + +- Support downstream Xbox SDK specifying networking initialization mechanism. ([#1359](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1359)) +- Added `crashpad` support infrastructure for the external crash reporter feature. ([#1375](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1375), [crashpad#131](https://github-redirect.dependabot.com/getsentry/crashpad/pull/131)) + +**Docs**: + +- Document the CMake 4 requirement on macOS `SDKROOT` due to its empty default for `CMAKE_OSX_SYSROOT` in the `README`. ([#1368](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1368)) + +**Thank you**: + +- [JanFellner](https://github-redirect.dependabot.com/JanFellner) + +### 0.10.1 + +**Internal**: + +- Correctly apply dynamic mutex initialization in unit-tests (fixes running unit-tests in downstream console SDKs). ([#1337](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1337)) + +### 0.10.0 + +**Breaking changes**: + +- By using transactions as automatic trace boundaries, transactions will, by default, no longer be part of the same singular trace. This is not the case when setting trace boundaries explicitly (`sentry_regenerate_trace()` or `sentry_set_trace()`), which turns off the automatic management of trace boundaries. ([#1270](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1270)) +- Change transaction sampling to be trace-based. This does not affect you when transactions are used for automatic trace boundaries (as described above), since every transaction is part of a new trace. However, if you manage trace boundaries manually (using `sentry_regenerate_trace()`) or run the Native SDK inside a downstream SDK like the Unity SDK, where these SDKs will manage the trace boundaries, for a given `traces_sample_rate`, either all transactions in a trace get sampled or none do with probability equal to that sample rate. ([#1254](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1254)) +- Moved Xbox toolchains to an Xbox-specific repository [sentry-xbox](https://github-redirect.dependabot.com/getsentry/sentry-xbox). You can request access to the repository by following the instructions in [Xbox documentation](https://docs.sentry.io/platforms/xbox/). ([#1329](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1329)) + +**Features**: + +- Add `sentry_clear_attachments()` to allow clearing all previously added attachments in the global scope. ([#1290](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1290)) +- Automatically set trace boundaries with every transaction. ([#1270](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1270)) +- Provide `sentry_regenerate_trace()` to allow users to set manual trace boundaries. ([#1293](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1293)) +- Add `Dynamic Sampling Context (DSC)` to events. ([#1254](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1254)) +- Add `sentry_value_new_feedback` and `sentry_capture_feedback` to allow capturing [User Feedback](https://develop.sentry.dev/sdk/data-model/envelope-items/#user-feedback). ([#1304](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1304)) + - Deprecate `sentry_value_new_user_feedback` and `sentry_capture_user_feedback` in favor of the new API. +- Add `sentry_envelope_read_from_file`, `sentry_envelope_get_header`, and `sentry_capture_envelope`. ([#1320](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1320)) +- Add `(u)int64` `sentry_value_t` type. ([#1326](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1326)) + +**Meta**: + +- Marked deprecated functions with `SENTRY_DEPRECATED(msg)`. ([#1308](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1308)) + +**Internal**: + +- Crash events from Crashpad now have `event_id` defined similarly to other backends. This makes it possible to associate feedback at the time of crash. ([#1319](https://github-redirect.dependabot.com/getsentry/sentry-native/pull/1319)) +'@ + + $actual | Should -Be $expected + } + + It 'handles commit SHA as OldTag by getting changelog diff when SHA does not map to tag' { + # Test with a SHA that doesn't correspond to any tag - should use diff approach + # This SHA is between v2.8.0 and v2.8.1 in github-workflows repo + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/github-workflows' ` + -OldTag 'cc24e8eb3c13d3d2e949f4a20c86d2ccac310c11' ` + -NewTag 'v2.8.1' + + $expected = @' +## Changelog + +### 2.8.1 +#### Fixes +- Sentry-CLI integration test - set server script root so assets access works. ([#63](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/63)) + +
+Full CHANGELOG.md diff + +```diff + -1,12 +1,10 + # Changelog + +-## Unreleased ++## 2.8.1 + +-### Dependencies ++### Fixes + +-- Bump CLI from v2.0.0 to v2.0.4 ([#60](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/60)) +- - [changelog](https://github-redirect.dependabot.com/getsentry/sentry-cli/blob/master/CHANGELOG.md[#204](https://github-redirect.dependabot.com/getsentry/github-workflows/issues/204)) +- - [diff](https://github-redirect.dependabot.com/getsentry/sentry-cli/compare/2.0.0...2.0.4) ++- Sentry-CLI integration test - set server script root so assets access works. ([#63](https://github-redirect.dependabot.com/getsentry/github-workflows/pull/63)) + + ## 2.8.0 + +``` + +
+'@ + + # there's an issue with line endings so we'll compare line by line + $actualLines = $actual -split "`n" + $expectedLines = $expected -split "`n" + $actualLines.Count | Should -Be $expectedLines.Count + for ($i = 0; $i -lt $actualLines.Count; $i++) { + $actualLines[$i].Trim() | Should -Be $expectedLines[$i].Trim() + } + } + + It 'falls back to git commits when no changelog files exist' { + # Test with a repository that doesn't have changelog files + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/responses.git' -OldTag '0.7.0' -NewTag '0.8.0' + + $expected = @' +## Changelog + +### Commits between 0.7.0 and 0.8.0 + +- Note passthru changes +- Add support for removing and replacing existing mocked URLs +- Add support for removing and replacing existing mocked URLs +- Use inspect.getfullargspec() in Python 3 +- ci: add codecov dep +- Changes for 0.7.0 +'@ + + $actual | Should -Be $expected + } + + It 'git commit fallback handles PR references correctly' { + # Test with a known repository and tags that contain PR references + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/responses.git' -OldTag '0.8.0' -NewTag '0.9.0' + + # This test verifies PR link formatting in commit messages + $expected = @' +## Changelog + +### Commits between 0.8.0 and 0.9.0 + +- Update CHANGES for 0.9.0 +- Merge pull request [#196](https://github-redirect.dependabot.com/getsentry/responses.git/issues/196) from getsentry/fix/python-37 +- fix: Adapt to re.Pattern in Python 3.7 +- test: Correct paths to artifacts +- test: Correct paths to artifacts +- test: Add Zeus +- Merge pull request [#192](https://github-redirect.dependabot.com/getsentry/responses.git/issues/192) from xmo-odoo/patch-1 +- force rebuild +- Merge pull request [#189](https://github-redirect.dependabot.com/getsentry/responses.git/issues/189) from wimglenn/issue_188 +- Add stream attribute to BaseResponse +- add 3.5 support +- add support for custom patch target +- Merge pull request [#187](https://github-redirect.dependabot.com/getsentry/responses.git/issues/187) from rmad17/master +- Update README.rst +- Adding installing section +- Merge pull request [#181](https://github-redirect.dependabot.com/getsentry/responses.git/issues/181) from feliperuhland/master +- Merge pull request [#178](https://github-redirect.dependabot.com/getsentry/responses.git/issues/178) from kathawala/unicode_passthru +- Fix README examples with import of requests library +- Satisfy linter +- Better test which doesn't rely on external requests +- Add unicode support for passthru urls +- Add support for unicode in domain names and tlds ([#177](https://github-redirect.dependabot.com/getsentry/responses.git/issues/177)) +- Attempt to satisfy linter +- All tests passed for fixing issue [#175](https://github-redirect.dependabot.com/getsentry/responses.git/issues/175) +- Adds unicode handling to BaseRequest init, fixes issue [#175](https://github-redirect.dependabot.com/getsentry/responses.git/issues/175) +- fix: Maintain 'method' param on 'add' +'@ + + $actual | Should -Be $expected + } + + It 'git commit fallback returns empty when no commits found' { + # Test with same tags (no commits between them) + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/responses.git' -OldTag '0.9.0' -NewTag '0.9.0' + + $actual | Should -BeNullOrEmpty + } + + It 'git commit fallback filters out version tag commits' { + # Test that version commits like "0.8.0" are filtered out + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/getsentry/responses.git' -OldTag '0.6.0' -NewTag '0.8.0' + + # Expected output should not contain version tag commits but should have meaningful commits + # This range includes version commits that should be filtered out + $expected = @' +## Changelog + +### Commits between 0.6.0 and 0.8.0 + +- Note passthru changes +- Add support for removing and replacing existing mocked URLs +- Add support for removing and replacing existing mocked URLs +- Use inspect.getfullargspec() in Python 3 +- ci: add codecov dep +- Changes for 0.7.0 +- Change behavior for multiple matches per PR comment +- Issue [#170](https://github-redirect.dependabot.com/getsentry/responses.git/issues/170): Fix bug with handling multiple matches +- ci: add codecov +- test: multiple urls same domain (refs GH-170) +- Changes for 0.6.2 +- compare query params length if match_querystring is set +- fix: ensuring default path if match_querystring is set +- update multiple responses example in README.rst +- fix: fix multiple responses +- fix: count mocked errors in RequestsMock +- fix: allow returning arbitrary status codes +- Changes for 0.6.1 +- Update README.rst +- drop support for Python 2.6 +- travis: dont setup pre-commit +- pre-commit 0.16.0 +- fix: restore adding_headers compatibility +- missing change refs +- Merge branch 'feature/do_not_remove_urls_when_assert_all_requests_are_fired' of https://github.com/j0hnsmith/responses into j0hnsmith-feature/do_not_remove_urls_when_assert_all_requests_are_fired +- The only change in behaviour when setting `assert_all_requests_are_fired=True` should be the expected assertion. +'@ + + $actual | Should -Be $expected + } + + It 'git commit fallback handles invalid repository gracefully' { + # Test with a non-existent repository to verify error handling + $actual = & "$PSScriptRoot/../scripts/get-changelog.ps1" ` + -RepoUrl 'https://github.com/nonexistent/repository' -OldTag 'v1.0.0' -NewTag 'v2.0.0' + + # Should return empty/null and not crash the script + $actual | Should -BeNullOrEmpty + } } diff --git a/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.expected b/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.expected new file mode 100644 index 0000000..8869c20 --- /dev/null +++ b/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.expected @@ -0,0 +1,16 @@ +# Changelog + +## Unreleased + +### Dependencies + +- Bump Dependency from v7.16.0 to v7.17.0 ([#123](https://github.com/getsentry/dependant/pulls/123)) + - [changelog](https://github.com/getsentry/dependency/blob/main/CHANGELOG.md#7170) + - [diff](https://github.com/getsentry/dependency/compare/7.16.0...7.17.0) + +## 0.14.0 + +### Dependencies + +This section contains only plain text with no bullet points. +The update-changelog script should handle this case gracefully. \ No newline at end of file diff --git a/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.original b/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.original new file mode 100644 index 0000000..6a9d64d --- /dev/null +++ b/updater/tests/testdata/changelog/no-bullet-points/CHANGELOG.md.original @@ -0,0 +1,8 @@ +# Changelog + +## 0.14.0 + +### Dependencies + +This section contains only plain text with no bullet points. +The update-changelog script should handle this case gracefully. diff --git a/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.expected b/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.expected new file mode 100644 index 0000000..9c3a9c6 --- /dev/null +++ b/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.expected @@ -0,0 +1,55 @@ +# Changelog + +## Unreleased + +### Breaking Changes + +Updater and Danger reusable workflows are now composite actions ([#114](https://github.com/getsentry/github-workflows/pull/114)) + +To update your existing Updater workflows: +```yaml +### Before + native: + uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + secrets: + # If a custom token is used instead, a CI would be triggered on a created PR. + api-token: ${{ secrets.CI_DEPLOY_KEY }} + +### After + native: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + api-token: ${{ secrets.CI_DEPLOY_KEY }} +``` + +To update your existing Danger workflows: +```yaml +### Before + danger: + uses: getsentry/github-workflows/.github/workflows/danger.yml@v2 + +### After + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 +``` + +### Dependencies + +- Bump Dependency from v7.16.0 to v7.17.0 ([#123](https://github.com/getsentry/dependant/pulls/123)) + - [changelog](https://github.com/getsentry/dependency/blob/main/CHANGELOG.md#7170) + - [diff](https://github.com/getsentry/dependency/compare/7.16.0...7.17.0) + +## 2.14.1 + +### Features + +- Do something ([#100](https://github.com/getsentry/dependant/pulls/100)) diff --git a/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.original b/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.original new file mode 100644 index 0000000..1f7cbc7 --- /dev/null +++ b/updater/tests/testdata/changelog/plain-text-intro/CHANGELOG.md.original @@ -0,0 +1,49 @@ +# Changelog + +## Unreleased + +### Breaking Changes + +Updater and Danger reusable workflows are now composite actions ([#114](https://github.com/getsentry/github-workflows/pull/114)) + +To update your existing Updater workflows: +```yaml +### Before + native: + uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + secrets: + # If a custom token is used instead, a CI would be triggered on a created PR. + api-token: ${{ secrets.CI_DEPLOY_KEY }} + +### After + native: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/updater@v3 + with: + path: scripts/update-sentry-native-ndk.sh + name: Native SDK + api-token: ${{ secrets.CI_DEPLOY_KEY }} +``` + +To update your existing Danger workflows: +```yaml +### Before + danger: + uses: getsentry/github-workflows/.github/workflows/danger.yml@v2 + +### After + danger: + runs-on: ubuntu-latest + steps: + - uses: getsentry/github-workflows/danger@v3 +``` + +## 2.14.1 + +### Features + +- Do something ([#100](https://github.com/getsentry/dependant/pulls/100)) diff --git a/updater/tests/update-changelog.Tests.ps1 b/updater/tests/update-changelog.Tests.ps1 index aa6d3a2..2665c42 100644 --- a/updater/tests/update-changelog.Tests.ps1 +++ b/updater/tests/update-changelog.Tests.ps1 @@ -1,6 +1,4 @@ -$testCases = - Describe 'update-changelog' { It '<_>' -ForEach @(Get-ChildItem "$PSScriptRoot/testdata/changelog/") { $testCase = $_ @@ -17,4 +15,37 @@ Describe 'update-changelog' { Get-Content "$testCase/CHANGELOG.md" | Should -Be (Get-Content "$testCase/CHANGELOG.md.expected") } + + It 'should correctly detect bullet points when plain text appears before bullet points' { + $testCasePath = "$PSScriptRoot/testdata/changelog/plain-text-intro" + Copy-Item "$testCasePath/CHANGELOG.md.original" "$testCasePath/CHANGELOG.md" + + pwsh -WorkingDirectory $testCasePath -File "$PSScriptRoot/../scripts/update-changelog.ps1" ` + -Name 'Dependency' ` + -PR 'https://github.com/getsentry/dependant/pulls/123' ` + -RepoUrl 'https://github.com/getsentry/dependency' ` + -MainBranch 'main' ` + -OldTag '7.16.0' ` + -NewTag '7.17.0' ` + -Section 'Dependencies' + + # verify the full output matches expected + Get-Content "$testCasePath/CHANGELOG.md" | Should -Be (Get-Content "$testCasePath/CHANGELOG.md.expected") + } + + It 'should handle changelogs with no bullet points by defaulting to dash' { + $testCasePath = "$PSScriptRoot/testdata/changelog/no-bullet-points" + Copy-Item "$testCasePath/CHANGELOG.md.original" "$testCasePath/CHANGELOG.md" + + pwsh -WorkingDirectory $testCasePath -File "$PSScriptRoot/../scripts/update-changelog.ps1" ` + -Name 'Dependency' ` + -PR 'https://github.com/getsentry/dependant/pulls/123' ` + -RepoUrl 'https://github.com/getsentry/dependency' ` + -MainBranch 'main' ` + -OldTag '7.16.0' ` + -NewTag '7.17.0' ` + -Section 'Dependencies' + + Get-Content "$testCasePath/CHANGELOG.md" | Should -Be (Get-Content "$testCasePath/CHANGELOG.md.expected") + } } diff --git a/updater/tests/update-dependency-cmake.Tests.ps1 b/updater/tests/update-dependency-cmake.Tests.ps1 new file mode 100644 index 0000000..cb9343d --- /dev/null +++ b/updater/tests/update-dependency-cmake.Tests.ps1 @@ -0,0 +1,347 @@ +BeforeAll { + # Load CMake helper functions from the main script + . "$PSScriptRoot/../scripts/cmake-functions.ps1" +} + +Describe 'Parse-CMakeFetchContent' { + Context 'Basic single dependency file' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:basicFile = "$tempDir/basic.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $basicFile + } + + It 'parses with explicit dependency name' { + $result = Parse-CMakeFetchContent $basicFile 'sentry-native' + + $result.GitRepository | Should -Be 'https://github.com/getsentry/sentry-native' + $result.GitTag | Should -Be 'v0.9.1' + $result.DepName | Should -Be 'sentry-native' + } + + It 'auto-detects single dependency' { + $result = Parse-CMakeFetchContent $basicFile $null + + $result.GitRepository | Should -Be 'https://github.com/getsentry/sentry-native' + $result.GitTag | Should -Be 'v0.9.1' + $result.DepName | Should -Be 'sentry-native' + } + + It 'throws on missing dependency' { + { Parse-CMakeFetchContent $basicFile 'nonexistent' } | Should -Throw "*FetchContent_Declare for 'nonexistent' not found*" + } + } + + Context 'Hash-based dependency file' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:hashFile = "$tempDir/hash.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2 # 0.9.1 + GIT_SHALLOW FALSE + GIT_SUBMODULES "external/breakpad" +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $hashFile + } + + It 'handles hash values correctly' { + $result = Parse-CMakeFetchContent $hashFile 'sentry-native' + + $result.GitRepository | Should -Be 'https://github.com/getsentry/sentry-native' + $result.GitTag | Should -Be 'a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2' + $result.DepName | Should -Be 'sentry-native' + } + } + + Context 'Complex formatting file' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:complexFile = "$tempDir/complex.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY + https://github.com/getsentry/sentry-native + GIT_TAG + v0.9.1 + GIT_SHALLOW + FALSE + GIT_SUBMODULES + "external/breakpad" +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $complexFile + } + + It 'handles complex multi-line formatting' { + $result = Parse-CMakeFetchContent $complexFile 'sentry-native' + + $result.GitRepository | Should -Be 'https://github.com/getsentry/sentry-native' + $result.GitTag | Should -Be 'v0.9.1' + $result.DepName | Should -Be 'sentry-native' + } + } + + Context 'Multiple dependencies file' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:multipleFile = "$tempDir/multiple.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 +) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest + GIT_TAG v1.14.0 +) + +FetchContent_MakeAvailable(sentry-native googletest) +'@ | Out-File $multipleFile + } + + It 'throws on multiple dependencies without explicit name' { + { Parse-CMakeFetchContent $multipleFile $null } | Should -Throw '*Multiple FetchContent declarations found*' + } + + It 'handles specific dependency from multiple dependencies' { + $result = Parse-CMakeFetchContent $multipleFile 'googletest' + + $result.GitRepository | Should -Be 'https://github.com/google/googletest' + $result.GitTag | Should -Be 'v1.14.0' + $result.DepName | Should -Be 'googletest' + } + } + + Context 'Malformed files' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:missingRepoFile = "$tempDir/missing-repo.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_TAG v0.9.1 +) +'@ | Out-File $missingRepoFile + + $script:missingTagFile = "$tempDir/missing-tag.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native +) +'@ | Out-File $missingTagFile + } + + It 'throws on missing GIT_REPOSITORY' { + { Parse-CMakeFetchContent $missingRepoFile 'sentry-native' } | Should -Throw '*Could not parse GIT_REPOSITORY or GIT_TAG*' + } + + It 'throws on missing GIT_TAG' { + { Parse-CMakeFetchContent $missingTagFile 'sentry-native' } | Should -Throw '*Could not parse GIT_REPOSITORY or GIT_TAG*' + } + } +} + +Describe 'Find-TagForHash' { + Context 'Hash resolution scenarios' { + It 'returns null for hash without matching tag' { + # Use a fake hash that won't match any real tag + $fakeHash = 'abcdef1234567890abcdef1234567890abcdef12' + $repo = 'https://github.com/getsentry/sentry-native' + + $result = Find-TagForHash $repo $fakeHash + + $result | Should -BeNullOrEmpty + } + + It 'handles network failures gracefully' { + $invalidRepo = 'https://github.com/nonexistent/repo' + $hash = 'abcdef1234567890abcdef1234567890abcdef12' + + # Should not throw, but return null and show warning + $result = Find-TagForHash $invalidRepo $hash + + $result | Should -BeNullOrEmpty + } + + # Note: Testing actual hash resolution requires network access + # and is better suited for integration tests + } +} + +Describe 'Update-CMakeFile' { + Context 'Basic tag updates' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-update-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:basicTemplate = @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(sentry-native) +'@ + } + + BeforeEach { + $script:basicTestFile = "$tempDir/basic-test.cmake" + } + + It 'updates tag to tag preserving format' { + $basicTemplate | Out-File $basicTestFile + + Update-CMakeFile $basicTestFile 'sentry-native' 'v0.9.2' + + $content = Get-Content $basicTestFile -Raw + $content | Should -Match 'GIT_TAG v0.9.2' + $content | Should -Not -Match 'v0.9.1' + } + + It 'preserves file structure and other content' { + $basicTemplate | Out-File $basicTestFile + + Update-CMakeFile $basicTestFile 'sentry-native' 'v0.9.2' + + $content = Get-Content $basicTestFile -Raw + $content | Should -Match 'include\(FetchContent\)' + $content | Should -Match 'FetchContent_MakeAvailable' + $content | Should -Match 'GIT_REPOSITORY https://github.com/getsentry/sentry-native' + $content | Should -Match 'GIT_SHALLOW FALSE' + } + + It 'throws on failed regex match' { + $basicTemplate | Out-File $basicTestFile + + # Try to update a dependency that doesn't exist + { Update-CMakeFile $basicTestFile 'nonexistent-dep' 'v1.0.0' } | Should -Throw "*FetchContent_Declare for 'nonexistent-dep' not found*" + } + } + + Context 'Hash updates' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-update-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:hashTemplate = @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2 # 0.9.1 + GIT_SHALLOW FALSE + GIT_SUBMODULES "external/breakpad" +) + +FetchContent_MakeAvailable(sentry-native) +'@ + } + + BeforeEach { + $script:hashTestFile = "$tempDir/hash-test.cmake" + } + + It 'updates hash to newer hash preserving format' { + $hashTemplate | Out-File $hashTestFile + + # Update to a newer tag that will be converted to hash (0.11.0 is known to exist) + Update-CMakeFile $hashTestFile 'sentry-native' '0.11.0' + + $content = Get-Content $hashTestFile -Raw + # Should have new hash with tag comment + $content | Should -Match 'GIT_TAG 3bd091313ae97be90be62696a2babe591a988eb8 # 0.11.0' + # Should not have old hash or old comment + $content | Should -Not -Match 'a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2' + $content | Should -Not -Match '# 0.9.1' + } + } + + Context 'Complex formatting' { + BeforeAll { + $script:tempDir = "$TestDrive/cmake-update-tests" + New-Item $tempDir -ItemType Directory -Force | Out-Null + + $script:complexTemplate = @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY + https://github.com/getsentry/sentry-native + GIT_TAG + v0.9.1 + GIT_SHALLOW + FALSE + GIT_SUBMODULES + "external/breakpad" +) + +FetchContent_MakeAvailable(sentry-native) +'@ + } + + BeforeEach { + $script:complexTestFile = "$tempDir/complex-test.cmake" + } + + It 'handles complex formatting correctly' { + $complexTemplate | Out-File $complexTestFile + + Update-CMakeFile $complexTestFile 'sentry-native' 'v0.9.2' + + $content = Get-Content $complexTestFile -Raw + $content | Should -Match 'GIT_TAG\s+v0.9.2' + $content | Should -Not -Match 'v0.9.1' + } + } + + # Note: Hash update tests require network access for git ls-remote + # and are better suited for integration tests +} diff --git a/updater/tests/update-dependency.Tests.ps1 b/updater/tests/update-dependency.Tests.ps1 index 3d1c9fe..25bfbed 100644 --- a/updater/tests/update-dependency.Tests.ps1 +++ b/updater/tests/update-dependency.Tests.ps1 @@ -1,7 +1,12 @@ BeforeAll { - function UpdateDependency([Parameter(Mandatory = $true)][string] $path, [string] $pattern = $null) + function UpdateDependency([Parameter(Mandatory = $true)][string] $path, [string] $pattern = $null, [string] $ghTitlePattern = $null, [string] $postUpdateScript = $null) { - $result = & "$PSScriptRoot/../scripts/update-dependency.ps1" -Path $path -Pattern $pattern + $params = @{ Path = $path } + if ($pattern) { $params.Pattern = $pattern } + if ($ghTitlePattern) { $params.GhTitlePattern = $ghTitlePattern } + if ($postUpdateScript) { $params.PostUpdateScript = $postUpdateScript } + + $result = & "$PSScriptRoot/../scripts/update-dependency.ps1" @params if (-not $?) { throw $result @@ -16,10 +21,13 @@ BeforeAll { } $repoUrl = 'https://github.com/getsentry/github-workflows' - # Find the latest latest version in this repo. We're intentionally using different code than `update-dependency.ps1` - # script uses to be able to catch issues, if any. - $currentVersion = (git -c 'versionsort.suffix=-' ls-remote --tags --sort='v:refname' $repoUrl ` - | Select-Object -Last 1 | Select-String -Pattern 'refs/tags/(.*)$').Matches.Groups[1].Value + # Find the latest latest version in this repo using the same logic as update-dependency.ps1 + . "$PSScriptRoot/../scripts/common.ps1" + [string[]]$tags = $(git ls-remote --refs --tags $repoUrl) + $tags = $tags | ForEach-Object { ($_ -split '\s+')[1] -replace '^refs/tags/', '' } + $tags = $tags -match '^v?([0-9.]+)$' + $tags = & "$PSScriptRoot/../scripts/sort-versions.ps1" $tags + $currentVersion = $tags[-1] } Describe ('update-dependency') { @@ -247,4 +255,414 @@ switch ($action) } } } + + Context 'cmake-fetchcontent' { + BeforeAll { + $cmakeTestDir = "$testDir/cmake" + if (-not (Test-Path $cmakeTestDir)) { + New-Item $cmakeTestDir -ItemType Directory + } + } + + It 'updates CMake file with explicit dependency name' { + $testFile = "$cmakeTestDir/sentry-explicit.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $testFile + + UpdateDependency "$testFile#sentry-native" + + $content = Get-Content $testFile -Raw + $content | Should -Not -Match 'v0.9.1' + $content | Should -Match 'GIT_TAG \d+\.\d+\.\d+' + $content | Should -Match 'GIT_REPOSITORY https://github.com/getsentry/sentry-native' + } + + It 'auto-detects single FetchContent dependency' { + $testFile = "$cmakeTestDir/sentry-auto.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.0 + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $testFile + + UpdateDependency $testFile + + $content = Get-Content $testFile -Raw + $content | Should -Not -Match 'v0.9.0' + $content | Should -Match 'GIT_TAG \d+\.\d+\.\d+' + } + + It 'updates from hash to newer tag preserving hash format' { + $testFile = "$cmakeTestDir/sentry-hash.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2 # 0.9.1 + GIT_SHALLOW FALSE +) + +FetchContent_MakeAvailable(sentry-native) +'@ | Out-File $testFile + + UpdateDependency $testFile + + $content = Get-Content $testFile -Raw + # Should update to a new hash with tag comment + $content | Should -Match 'GIT_TAG [a-f0-9]{40} # \d+\.\d+\.\d+' + $content | Should -Not -Match 'a64d5bd8ee130f2cda196b6fa7d9b65bfa6d32e2' + } + + It 'handles multiple dependencies with explicit selection' { + $testFile = "$cmakeTestDir/multiple-deps.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 +) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest + GIT_TAG v1.14.0 +) + +FetchContent_MakeAvailable(sentry-native googletest) +'@ | Out-File $testFile + + UpdateDependency "$testFile#googletest" + + $content = Get-Content $testFile -Raw + # sentry-native should remain unchanged + $content | Should -Match 'sentry-native[\s\S]*GIT_TAG v0\.9\.1' + # googletest should be updated + $content | Should -Match 'googletest[\s\S]*GIT_TAG v1\.\d+\.\d+' + $content | Should -Not -Match 'googletest[\s\S]*GIT_TAG v1\.14\.0' + } + + It 'outputs correct GitHub Actions variables' { + $testFile = "$cmakeTestDir/output-test.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.0 +) +'@ | Out-File $testFile + + $output = UpdateDependency $testFile + + # Join output lines for easier searching + $outputText = $output -join "`n" + $outputText | Should -Match 'originalTag=v0\.9\.0' + $outputText | Should -Match 'latestTag=\d+\.\d+\.\d+' + $outputText | Should -Match 'url=https://github.com/getsentry/sentry-native' + $outputText | Should -Match 'mainBranch=master' + } + + It 'respects version patterns' { + $testFile = "$cmakeTestDir/pattern-test.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.8.0 +) +'@ | Out-File $testFile + + # Limit to 0.9.x versions + UpdateDependency $testFile '^v?0\.9\.' + + $content = Get-Content $testFile -Raw + $content | Should -Match 'GIT_TAG 0\.9\.\d+' + $content | Should -Not -Match 'v0\.8\.0' + } + + It 'fails on multiple dependencies without explicit name' { + $testFile = "$cmakeTestDir/multi-fail.cmake" + @' +include(FetchContent) + +FetchContent_Declare(sentry-native GIT_REPOSITORY https://github.com/getsentry/sentry-native GIT_TAG v0.9.1) +FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest GIT_TAG v1.14.0) +'@ | Out-File $testFile + + { UpdateDependency $testFile } | Should -Throw '*Multiple FetchContent declarations found*' + } + + It 'fails on missing dependency' { + $testFile = "$cmakeTestDir/missing-dep.cmake" + @' +include(FetchContent) + +FetchContent_Declare( + sentry-native + GIT_REPOSITORY https://github.com/getsentry/sentry-native + GIT_TAG v0.9.1 +) +'@ | Out-File $testFile + + { UpdateDependency "$testFile#nonexistent" } | Should -Throw "*FetchContent_Declare for 'nonexistent' not found*" + } + } + + Context 'gh-title-pattern' { + It 'filters by GitHub release title pattern' { + $testFile = "$testDir/test.properties" + # Use sentry-cocoa repo which has releases with "(Stable)" suffix + $repo = 'https://github.com/getsentry/sentry-cocoa' + @("repo=$repo", 'version=0') | Out-File $testFile + + # Test filtering for releases with "(Stable)" suffix + UpdateDependency $testFile '' '\(Stable\)$' + + $content = Get-Content $testFile + $version = ($content | Where-Object { $_ -match '^version\s*=\s*(.+)$' }) -replace '^version\s*=\s*', '' + + # Verify that a version was selected (should be a stable release) + $version | Should -Not -Be '0' + $version | Should -Match '^\d+\.\d+\.\d+$' + } + + It 'throws error when no releases match title pattern' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/github-workflows' + @("repo=$repo", 'version=0') | Out-File $testFile + + # Use a pattern that should match no releases + { UpdateDependency $testFile '' 'NonExistentPattern' } | Should -Throw '*Found no tags with GitHub releases matching title pattern*' + } + + It 'matches specific release version by exact title pattern' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/github-workflows' + @("repo=$repo", 'version=0') | Out-File $testFile + + # Target a specific known release by exact title match + UpdateDependency $testFile '' '^2\.11\.1$' + + $content = Get-Content $testFile + $version = ($content | Where-Object { $_ -match '^version\s*=\s*(.+)$' }) -replace '^version\s*=\s*', '' + + # Should get exactly version 2.11.1 (with or without 'v' prefix) + $version | Should -Match '^v?2\.11\.1$' + } + + It 'works without title pattern (backward compatibility)' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cocoa' + @("repo=$repo", 'version=0') | Out-File $testFile + + # Test without title pattern should work as before + UpdateDependency $testFile '^8\.' + + $content = Get-Content $testFile + $version = ($content | Where-Object { $_ -match '^version\s*=\s*(.+)$' }) -replace '^version\s*=\s*', '' + + # Should get a version starting with 8 + $version | Should -Match '^8\.' + } + } + + Context 'post-update-script' { + It 'runs PowerShell post-update script with version arguments' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0') | Out-File $testFile + + $postUpdateScript = "$testDir/post-update-test.ps1" + $markerFile = "$testDir/post-update-marker.txt" + @' +param([string] $originalVersion, [string] $newVersion) +"$originalVersion|$newVersion" | Out-File +'@ + " '$markerFile'" | Out-File $postUpdateScript + + UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript + + # Verify post-update script was executed + Test-Path $markerFile | Should -Be $true + $markerContent = Get-Content $markerFile + $markerContent | Should -Match '^0\|0\.28\.0$' + + # Clean up + Remove-Item $markerFile -ErrorAction SilentlyContinue + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'runs PowerShell post-update script when Tag and OriginalTag are explicitly provided' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0.27.0') | Out-File $testFile + + $postUpdateScript = "$testDir/post-update-explicit.ps1" + $markerFile = "$testDir/post-update-marker-explicit.txt" + @' +param([string] $originalVersion, [string] $newVersion) +"$originalVersion|$newVersion" | Out-File +'@ + " '$markerFile'" | Out-File $postUpdateScript + + # Simulate the second run where we explicitly set Tag and OriginalTag + $params = @{ + Path = $testFile + Tag = '0.28.0' + OriginalTag = '0.27.0' + PostUpdateScript = $postUpdateScript + } + $result = & "$PSScriptRoot/../scripts/update-dependency.ps1" @params + if (-not $?) { + throw $result + } + + # Verify post-update script was executed with correct versions + Test-Path $markerFile | Should -Be $true + $markerContent = Get-Content $markerFile + $markerContent | Should -Match '^0\.27\.0\|0\.28\.0$' + + # Clean up + Remove-Item $markerFile -ErrorAction SilentlyContinue + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'fails when Tag is provided without OriginalTag' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0.27.0') | Out-File $testFile + + $postUpdateScript = "$testDir/post-update-fail.ps1" + @' +param([string] $originalVersion, [string] $newVersion) +"$originalVersion|$newVersion" | Out-File marker.txt +'@ | Out-File $postUpdateScript + + # This should fail because Tag requires OriginalTag + $params = @{ + Path = $testFile + Tag = '0.28.0' + PostUpdateScript = $postUpdateScript + } + { & "$PSScriptRoot/../scripts/update-dependency.ps1" @params } | Should -Throw '*Expected*to be different*' + + # Clean up + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'runs bash post-update script with version arguments' -Skip:$IsWindows { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0') | Out-File $testFile + + $postUpdateScript = "$testDir/post-update-test.sh" + $markerFile = "$testDir/post-update-marker.txt" + @" +#!/usr/bin/env bash +set -euo pipefail +echo "`$1|`$2" > '$markerFile' +"@ | Out-File $postUpdateScript + + UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript + + # Verify post-update script was executed + Test-Path $markerFile | Should -Be $true + $markerContent = Get-Content $markerFile + $markerContent | Should -Match '^0\|0\.28\.0$' + + # Clean up + Remove-Item $markerFile -ErrorAction SilentlyContinue + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'fails when post-update script does not exist' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0') | Out-File $testFile + + $postUpdateScript = "$testDir/nonexistent-script.ps1" + + { UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript } | Should -Throw '*Post-update script not found*' + } + + It 'fails when PowerShell post-update script exits with error' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0') | Out-File $testFile + + $postUpdateScript = "$testDir/failing-post-update.ps1" + @' +param([string] $originalVersion, [string] $newVersion) +throw "Post-update script failed intentionally" +'@ | Out-File $postUpdateScript + + { UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript } | Should -Throw '*Post-update script failed*' + + # Clean up + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'fails when bash post-update script exits with error' -Skip:$IsWindows { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=0') | Out-File $testFile + + $postUpdateScript = "$testDir/failing-post-update.sh" + @' +#!/usr/bin/env bash +exit 1 +'@ | Out-File $postUpdateScript + + { UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript } | Should -Throw '*Post-update script failed*' + + # Clean up + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + + It 'receives empty string for original version when updating from scratch' { + $testFile = "$testDir/test.properties" + $repo = 'https://github.com/getsentry/sentry-cli' + @("repo=$repo", 'version=') | Out-File $testFile + + $postUpdateScript = "$testDir/post-update-empty-original.ps1" + $markerFile = "$testDir/post-update-marker-empty.txt" + @' +param([string] $originalVersion, [string] $newVersion) +"original=[$originalVersion]|new=[$newVersion]" | Out-File +'@ + " '$markerFile'" | Out-File $postUpdateScript + + UpdateDependency $testFile '^0\.' -postUpdateScript $postUpdateScript + + # Verify post-update script received empty original version + Test-Path $markerFile | Should -Be $true + $markerContent = Get-Content $markerFile + $markerContent | Should -Match 'original=\[\]\|new=\[0\.28\.0\]' + + # Clean up + Remove-Item $markerFile -ErrorAction SilentlyContinue + Remove-Item $postUpdateScript -ErrorAction SilentlyContinue + } + } } diff --git a/updater/tests/workflow-args.sh b/updater/tests/workflow-args.sh old mode 100644 new mode 100755 index 6b8d1d4..8d68c11 --- a/updater/tests/workflow-args.sh +++ b/updater/tests/workflow-args.sh @@ -5,7 +5,21 @@ set -euo pipefail case $1 in get-version) - echo "latest" + # Return the actual latest tag to ensure no update is needed + # Always use remote lookup for consistency with update-dependency.ps1 + tags=$(git ls-remote --tags --refs https://github.com/getsentry/github-workflows.git | \ + sed 's/.*refs\/tags\///' | \ + grep -E '^v?[0-9.]+$') + + # Sort by version number, handling mixed v prefixes + latest=$(echo "$tags" | sed 's/^v//' | sort -V | tail -1) + + # Check if original had v prefix and restore it + if echo "$tags" | grep -q "^v$latest$"; then + echo "v$latest" + else + echo "$latest" + fi # Run actual tests here. if [[ "$(uname)" != 'Darwin' ]]; then