From 0864f1ac956c743702a73d1dca2ad590627759d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:48:58 -0400 Subject: [PATCH 1/9] chore(deps): bump gitlab.com/gitlab-org/api/client-go from 0.131.0 to 0.132.0 (#23647) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 48448a92a3674..3d4eb1651f45c 100644 --- a/go.mod +++ b/go.mod @@ -85,7 +85,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/valyala/fasttemplate v1.2.2 github.com/yuin/gopher-lua v1.1.1 - gitlab.com/gitlab-org/api/client-go v0.131.0 + gitlab.com/gitlab-org/api/client-go v0.132.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/otel v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 diff --git a/go.sum b/go.sum index f7411314fdd2f..6a89e41600826 100644 --- a/go.sum +++ b/go.sum @@ -903,8 +903,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -gitlab.com/gitlab-org/api/client-go v0.131.0 h1:a431AKWkrSO5dgY5o5okFjxpANhkGpzxnZrjAHRyqp8= -gitlab.com/gitlab-org/api/client-go v0.131.0/go.mod h1:U83AmpPrAir8NH31T/BstwZcJzS/nGZptOXtGjPZrbI= +gitlab.com/gitlab-org/api/client-go v0.132.0 h1:6W4VAmbWVbjUEoQiybPAn6bMP5v0Ga9jeTJaRtc7zfI= +gitlab.com/gitlab-org/api/client-go v0.132.0/go.mod h1:U83AmpPrAir8NH31T/BstwZcJzS/nGZptOXtGjPZrbI= go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM= go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= From 354bbfd71aa42a5c9c0dcd86290ab0f5bc6b1693 Mon Sep 17 00:00:00 2001 From: Nebojsa Prodana Date: Mon, 2 Jun 2025 18:37:39 +0200 Subject: [PATCH 2/9] DODO-2265: Prep fork --- .github/ISSUE_TEMPLATE/bug_report.md | 43 -- .github/ISSUE_TEMPLATE/config.yml | 12 - .../ISSUE_TEMPLATE/enhancement_proposal.md | 18 - .github/ISSUE_TEMPLATE/new_dev_tool.md | 43 -- .github/ISSUE_TEMPLATE/release.md | 26 - .github/ISSUE_TEMPLATE/security_logs.md | 19 - .github/cherry-pick-bot.yml | 3 - .github/dependabot.yml | 60 -- .github/no-response.yml | 1 - .github/pr-title-checker-config.json | 15 - .github/stale.yml | 4 - .github/workflows/README.md | 8 - .github/workflows/bump-major-version.yaml | 89 --- .github/workflows/ci-build.yaml | 73 ++- .github/workflows/codeql.yml | 64 --- .github/workflows/image.yaml | 111 ++-- .github/workflows/init-fix-branch.yaml | 37 ++ .github/workflows/init-release.yaml | 77 --- .github/workflows/pr-title-check.yml | 29 - .github/workflows/promote-fix-branch.yaml | 37 ++ .github/workflows/scorecard.yaml | 67 --- .github/workflows/update-snyk.yaml | 36 -- Dockerfile | 5 +- tools/fork-cli/INTERNAL_CONTRIB.md | 209 +++++++ tools/fork-cli/main.go | 536 ++++++++++++++++++ tools/fork-cli/main_test.go | 482 ++++++++++++++++ 26 files changed, 1414 insertions(+), 690 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/config.yml delete mode 100644 .github/ISSUE_TEMPLATE/enhancement_proposal.md delete mode 100644 .github/ISSUE_TEMPLATE/new_dev_tool.md delete mode 100644 .github/ISSUE_TEMPLATE/release.md delete mode 100644 .github/ISSUE_TEMPLATE/security_logs.md delete mode 100644 .github/cherry-pick-bot.yml delete mode 100644 .github/dependabot.yml delete mode 100644 .github/no-response.yml delete mode 100644 .github/pr-title-checker-config.json delete mode 100644 .github/stale.yml delete mode 100644 .github/workflows/bump-major-version.yaml delete mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/init-fix-branch.yaml delete mode 100644 .github/workflows/init-release.yaml delete mode 100644 .github/workflows/pr-title-check.yml create mode 100644 .github/workflows/promote-fix-branch.yaml delete mode 100644 .github/workflows/scorecard.yaml delete mode 100644 .github/workflows/update-snyk.yaml create mode 100644 tools/fork-cli/INTERNAL_CONTRIB.md create mode 100644 tools/fork-cli/main.go create mode 100644 tools/fork-cli/main_test.go diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 41a1b4ae95ec7..0000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: 'bug' -assignees: '' ---- - - - -Checklist: - -* [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq. -* [ ] I've included steps to reproduce the bug. -* [ ] I've pasted the output of `argocd version`. - -**Describe the bug** - - - -**To Reproduce** - - - -**Expected behavior** - - - -**Screenshots** - - - -**Version** - -```shell -Paste the output from `argocd version` here. -``` - -**Logs** - -``` -Paste any relevant application logs here. -``` diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index b306343920656..0000000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,12 +0,0 @@ -blank_issues_enabled: false - -contact_links: - - name: Have you read the docs? - url: https://argo-cd.readthedocs.io/ - about: Much help can be found in the docs - - name: Ask a question - url: https://github.com/argoproj/argo-cd/discussions/new - about: Ask a question or start a discussion about Argo CD - - name: Chat on Slack - url: https://argoproj.github.io/community/join-slack - about: Maybe chatting with the community can help diff --git a/.github/ISSUE_TEMPLATE/enhancement_proposal.md b/.github/ISSUE_TEMPLATE/enhancement_proposal.md deleted file mode 100644 index 8ab874b3150b6..0000000000000 --- a/.github/ISSUE_TEMPLATE/enhancement_proposal.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Enhancement proposal -about: Propose an enhancement for this project -title: '' -labels: 'enhancement' -assignees: '' ---- -# Summary - -What change you think needs making. - -# Motivation - -Please give examples of your use case, e.g. when would you use this. - -# Proposal - -How do you think this should be implemented? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/new_dev_tool.md b/.github/ISSUE_TEMPLATE/new_dev_tool.md deleted file mode 100644 index 6100922376b9d..0000000000000 --- a/.github/ISSUE_TEMPLATE/new_dev_tool.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -name: New Dev Tool Request -about: This is a request for adding a new tool for setting up a dev environment. -title: '' -labels: '' -assignees: '' ---- - -Checklist: - -* [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is. -* [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool) -* [ ] I have a lead sponsor who is a core Argo CD maintainer -* [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree -* [ ] I have given a motivation why this should be added - -### The proposer - -<-- The username(s) of the person(s) proposing the tool --> - -### The proposed tool - - - -### Motivation - - - -### Link to PR (Optional) - - - -### Lead Sponsor(s) - -Final approval requires sponsorship from at least one core maintainer. - -- @ - -### Co-sponsors - -These will be the co-maintainers of the specified tool. - -- @ diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md deleted file mode 100644 index b43b91a0e05ce..0000000000000 --- a/.github/ISSUE_TEMPLATE/release.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: Argo CD Release -about: Used by our Release Champion to track progress of a minor release -title: 'Argo CD Release vX.X' -labels: 'release' -assignees: '' ---- - -Target RC1 date: ___. __, ____ -Target GA date: ___. __, ____ - - - [ ] 1wk before feature freeze post in #argo-contributors that PRs must be merged by DD-MM-YYYY to be included in the release - ask approvers to drop items from milestone they can’t merge - - [ ] At least two days before RC1 date, draft RC blog post and submit it for review (or delegate this task) - - [ ] Cut RC1 (or delegate this task to an Approver and coordinate timing) - - [ ] Create new release branch - - [ ] Add the release branch to ReadTheDocs - - [ ] Confirm that tweet and blog post are ready - - [ ] Trigger the release - - [ ] After the release is finished, publish tweet and blog post - - [ ] Post in #argo-cd and #argo-announcements with lots of emojis announcing the release and requesting help testing - - [ ] Monitor support channels for issues, cherry-picking bugfixes and docs fixes as appropriate (or delegate this task to an Approver and coordinate timing) - - [ ] At release date, evaluate if any bugs justify delaying the release. If not, cut the release (or delegate this task to an Approver and coordinate timing) - - [ ] If unreleased changes are on the release branch for {current minor version minus 3}, cut a final patch release for that series (or delegate this task to an Approver and coordinate timing) - - [ ] After the release, post in #argo-cd that the {current minor version minus 3} has reached EOL (example: https://cloud-native.slack.com/archives/C01TSERG0KZ/p1667336234059729) - - [ ] (For the next release champion) Review the [items scheduled for the next release](https://github.com/orgs/argoproj/projects/25). If any item does not have an assignee who can commit to finish the feature, move it to the next release. - - [ ] (For the next release champion) Schedule a time mid-way through the release cycle to review items again. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/security_logs.md b/.github/ISSUE_TEMPLATE/security_logs.md deleted file mode 100644 index bfb5d2f339c62..0000000000000 --- a/.github/ISSUE_TEMPLATE/security_logs.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Security log -about: Propose adding security-related logs or tagging existing logs with security fields -title: "seclog: [Event Description]" -labels: security-log -assignees: notfromstatefarm ---- -# Event to be logged - -Specify the event that needs to be logged or existing logs that need to be tagged. - -# Proposed level - -What security level should these events be logged under? Refer to https://argo-cd.readthedocs.io/en/latest/operator-manual/security/#security-field for more info. - -# Common Weakness Enumeration - -Is there an associated [CWE](https://cwe.mitre.org/) that could be tagged as well? - diff --git a/.github/cherry-pick-bot.yml b/.github/cherry-pick-bot.yml deleted file mode 100644 index a9de2afd04927..0000000000000 --- a/.github/cherry-pick-bot.yml +++ /dev/null @@ -1,3 +0,0 @@ -enabled: true -preservePullRequestTitle: true - diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f0850cec0475d..0000000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,60 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gomod" - directory: "/" - schedule: - interval: "daily" - open-pull-requests-limit: 20 - ignore: - - dependency-name: k8s.io/* - groups: - otel: - patterns: - - "go.opentelemetry.io/*" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "daily" - - - package-ecosystem: "npm" - directory: "/ui/" - schedule: - interval: "daily" - -# Disabled since this code is rarely used. -# - package-ecosystem: "npm" -# directory: "/ui-test/" -# schedule: -# interval: "daily" - - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "daily" - ignore: - # We use consistent go and node versions across a lot of different files, and updating via dependabot would cause - # drift among those files, instead we let renovate bot handle them. - - dependency-name: "library/golang" - - dependency-name: "library/node" - - - package-ecosystem: "docker" - directory: "/test/container/" - schedule: - interval: "daily" - - - package-ecosystem: "docker" - directory: "/test/e2e/multiarch-container/" - schedule: - interval: "daily" - - - package-ecosystem: "docker" - directory: "/test/remote/" - schedule: - interval: "daily" - -# Disabled since this code is rarely used. -# - package-ecosystem: "docker" -# directory: "/ui-test/" -# schedule: -# interval: "daily" diff --git a/.github/no-response.yml b/.github/no-response.yml deleted file mode 100644 index 47e7fb6fbf4ba..0000000000000 --- a/.github/no-response.yml +++ /dev/null @@ -1 +0,0 @@ -# See https://github.com/probot/no-response diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json deleted file mode 100644 index c3437def33834..0000000000000 --- a/.github/pr-title-checker-config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "LABEL": { - "name": "title needs formatting", - "color": "EEEEEE" - }, - "CHECKS": { - "prefixes": ["[Bot] docs: "], - "regexp": "^(feat|fix|docs|test|ci|chore)!?(\\(.*\\))?!?:.*" - }, - "MESSAGES": { - "success": "PR title is valid", - "failure": "PR title is invalid", - "notice": "PR Title needs to pass regex '^(feat|fix|docs|test|ci|chore)!?(\\(.*\\))?!?:.*" - } - } diff --git a/.github/stale.yml b/.github/stale.yml deleted file mode 100644 index 1d82d96d13288..0000000000000 --- a/.github/stale.yml +++ /dev/null @@ -1,4 +0,0 @@ -# See https://github.com/probot/stale -# See https://github.com/probot/stale -exemptLabels: - - backlog diff --git a/.github/workflows/README.md b/.github/workflows/README.md index b6348eae93a41..f3649f362d5c7 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -3,14 +3,8 @@ | Workflow | Description | |--------------------|----------------------------------------------------------------| | ci-build.yaml | Build, lint, test, codegen, build-ui, analyze, e2e-test | -| codeql.yaml | CodeQL analysis | | image-reuse.yaml | Build, push, and Sign container images | | image.yaml | Build container image for PR's & publish for push events | -| init-release.yaml | Build manifests and version then create a PR for release branch| -| pr-title-check.yaml| Lint PR for semantic information | -| release.yaml | Build images, cli-binaries, provenances, and post actions | -| scorecard.yaml | Generate scorecard for supply-chain security | -| update-snyk.yaml | Scheduled snyk reports | # Reusable workflows @@ -26,9 +20,7 @@ | Inputs | Description | Type | Required | Defaults | |-------------------|-------------------------------------|-------------|----------|-----------------| | go-version | Version of Go to be used | string | true | none | -| quay_image_name | Full image name and tag | CSV, string | false | none | | ghcr_image_name | Full image name and tag | CSV, string | false | none | -| docker_image_name | Full image name and tag | CSV, string | false | none | | platforms | Platforms to build (linux/amd64) | CSV, string | false | linux/amd64 | | push | Whether to push image/s to registry | boolean | false | false | | target | Target build stage | string | false | none | diff --git a/.github/workflows/bump-major-version.yaml b/.github/workflows/bump-major-version.yaml deleted file mode 100644 index 5058ceba6d1a5..0000000000000 --- a/.github/workflows/bump-major-version.yaml +++ /dev/null @@ -1,89 +0,0 @@ -name: Bump major version -on: - workflow_dispatch: {} - -permissions: {} - -jobs: - prepare-release: - permissions: - contents: write # for peter-evans/create-pull-request to create branch - pull-requests: write # for peter-evans/create-pull-request to create a PR - name: Automatically update major version - runs-on: ubuntu-22.04 - steps: - - name: Checkout code - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - # Get the current major version from go.mod and save it as a variable. - - name: Get target version - id: get-target-version - run: | - set -ue - CURRENT_VERSION=$(grep 'module github.com/argoproj/argo-cd' go.mod | awk '{print $2}' | sed 's/.*\/v//') - echo "TARGET_VERSION=$((CURRENT_VERSION + 1))" >> $GITHUB_OUTPUT - - - name: Copy source code to GOPATH - run: | - mkdir -p ~/go/src/github.com/argoproj - cp -a ../argo-cd ~/go/src/github.com/argoproj - - - name: Run script to bump the version - run: | - hack/bump-major-version.sh - working-directory: /home/runner/go/src/github.com/argoproj/argo-cd - - - name: Setup Golang - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version: ${{ env.GOLANG_VERSION }} - - name: Add ~/go/bin to PATH - run: | - echo "/home/runner/go/bin" >> $GITHUB_PATH - - name: Add /usr/local/bin to PATH - run: | - echo "/usr/local/bin" >> $GITHUB_PATH - - name: Download & vendor dependencies - run: | - # We need to vendor go modules for codegen yet - go mod download - go mod vendor -v - working-directory: /home/runner/go/src/github.com/argoproj/argo-cd - - name: Install toolchain for codegen - run: | - make install-codegen-tools-local - make install-go-tools-local - working-directory: /home/runner/go/src/github.com/argoproj/argo-cd - # We install kustomize in the dist directory - - name: Add dist to PATH - run: | - echo "/home/runner/work/argo-cd/argo-cd/dist" >> $GITHUB_PATH - - name: Run codegen - run: | - set -x - export GOPATH=$(go env GOPATH) - make codegen-local - working-directory: /home/runner/go/src/github.com/argoproj/argo-cd - - - name: Copy changes back - run: | - # Copy the contents back, but skip the .git directory - rsync -a --exclude=.git /home/runner/go/src/github.com/argoproj/argo-cd/ ../argo-cd - - - name: Create pull request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 - with: - commit-message: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}" - title: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}" - body: | - Congrats! You've just bumped the major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}. - - Next steps: - - [ ] Merge this PR - - [ ] Add an upgrade guide to the docs for this version - branch: bump-major-version - branch-suffix: random - signoff: true \ No newline at end of file diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index b9a37b36f045f..b9419914f6d27 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -1,15 +1,32 @@ name: Integration tests + on: push: branches: - - 'master' - - 'release-*' - - '!release-1.4' - - '!release-1.5' + # We use skyscanner-internal/master as the base branch for integration + # This branch contains our CI changes and is not meant for direct contributions. + # Our internal development is to be merged into here. + # The changes that are ready for contribution should be cherry-picked to skyscanner-contrib/master. + - skyscanner-internal/master + # Branches to be used for development and testing. They should be based on skyscanner-internal/master. + - skyscanner-internal/develop/** pull_request: branches: - - 'master' - - 'release-*' + - skyscanner-internal/master + # Cleaned up, ready for contribution PRs are to be cherry-picked here + - skyscanner-contrib/master + +# on: +# push: +# branches: +# - 'master' +# - 'release-*' +# - '!release-1.4' +# - '!release-1.5' +# pull_request: +# branches: +# - 'master' +# - 'release-*' env: # Golang version to use across CI steps @@ -327,7 +344,8 @@ jobs: NODE_ONLINE_ENV: online HOST_ARCH: amd64 # If we're on the master branch, set the codecov token so that we upload bundle analysis - CODECOV_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.CODECOV_TOKEN || '' }} + # TODO: Needs codecov, which we do not support yet + # CODECOV_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.CODECOV_TOKEN || '' }} working-directory: ui/ - name: Run ESLint run: yarn lint @@ -384,26 +402,27 @@ jobs: # references to their coverage output directories. run: | go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller,e2e-code-coverage/commit-server -o test-results/full-coverage.out - - name: Upload code coverage information to codecov.io - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 - with: - files: test-results/full-coverage.out - fail_ci_if_error: true - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - name: Upload test results to Codecov - if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'argoproj/argo-cd' - uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 - with: - file: test-results/junit.xml - fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} - - name: Perform static code analysis using SonarCloud - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5.2.0 - if: env.sonar_secret != '' + # TODO: Needs allowlisting + # - name: Upload code coverage information to codecov.io + # uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + # with: + # files: test-results/full-coverage.out + # fail_ci_if_error: true + # env: + # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # - name: Upload test results to Codecov + # if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'Skyscanner/argo-cd' + # uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 + # with: + # file: test-results/junit.xml + # fail_ci_if_error: true + # token: ${{ secrets.CODECOV_TOKEN }} + # - name: Perform static code analysis using SonarCloud + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5.2.0 + # if: env.sonar_secret != '' test-e2e: name: Run end-to-end tests if: ${{ needs.changes.outputs.backend == 'true' }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index c3278d80c823d..0000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: "Code scanning - action" - -on: - push: - # Secrets aren't available for dependabot on push. https://docs.github.com/en/enterprise-cloud@latest/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/troubleshooting-the-codeql-workflow#error-403-resource-not-accessible-by-integration-when-using-dependabot - branches-ignore: - - 'dependabot/**' - - 'cherry-pick-*' - pull_request: - schedule: - - cron: '0 19 * * 0' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - CodeQL-Build: - permissions: - actions: read # for github/codeql-action/init to get workflow details - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/autobuild to send a status report - if: github.repository == 'argoproj/argo-cd' || vars.enable_codeql - - # CodeQL runs on ubuntu-latest and windows-latest - runs-on: ubuntu-22.04 - steps: - - name: Checkout repository - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - - # Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 - - name: Setup Golang - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - with: - go-version-file: go.mod - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@8fcfedf57053e09257688fce7a0beeb18b1b9ae3 # v2.17.2 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@8fcfedf57053e09257688fce7a0beeb18b1b9ae3 # v2.17.2 - - # ℹ️ Command-line programs to run using the OS shell. - # πŸ“š https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8fcfedf57053e09257688fce7a0beeb18b1b9ae3 # v2.17.2 diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 5f1f7627fd077..4bdd760543928 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -3,10 +3,18 @@ name: Image on: push: branches: - - master + # We use skyscanner-internal/master as the base branch for integration + # This branch contains our CI changes and is not meant for direct contributions. + # Our internal development is to be merged into here. + # The changes that are ready for contribution should be cherry-picked to skyscanner-contrib/master. + - skyscanner-internal/master + # Branches to be used for development and testing. They should be based on skyscanner-internal/master. + - skyscanner-internal/develop/** pull_request: branches: - - master + - skyscanner-internal/master + # Cleaned up, ready for contribution PRs are to be cherry-picked here + - skyscanner-contrib/master types: [labeled, unlabeled, opened, synchronize, reopened] concurrency: @@ -19,7 +27,7 @@ jobs: set-vars: permissions: contents: read - if: github.repository == 'argoproj/argo-cd' + if: github.repository == 'Skyscanner/argo-cd' runs-on: ubuntu-22.04 outputs: image-tag: ${{ steps.image.outputs.tag}} @@ -28,7 +36,13 @@ jobs: - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - name: Set image tag for ghcr - run: echo "tag=$(cat ./VERSION)-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT + run: | + # Get the branch name + BRANCH_NAME=${GITHUB_REF#refs/heads/} + # Sanitize branch name for container registry compliance + SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | sed -e 's/\//-/g' | tr '[:upper:]' '[:lower:]') + # Set the image tag with sanitized branch name + echo "tag=$(cat ./VERSION)-${SANITIZED_BRANCH}-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT id: image - name: Determine image platforms to use @@ -37,7 +51,7 @@ jobs: IMAGE_PLATFORMS=linux/amd64 if [[ "${{ github.event_name }}" == "push" || "${{ contains(github.event.pull_request.labels.*.name, 'test-multi-image') }}" == "true" ]] then - IMAGE_PLATFORMS=linux/amd64,linux/arm64,linux/s390x,linux/ppc64le + IMAGE_PLATFORMS=linux/amd64,linux/arm64 fi echo "Building image for platforms: $IMAGE_PLATFORMS" echo "platforms=$IMAGE_PLATFORMS" >> $GITHUB_OUTPUT @@ -48,7 +62,7 @@ jobs: contents: read packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags id-token: write # for creating OIDC tokens for signing. - if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name != 'push' }} + if: ${{ github.repository == 'Skyscanner/argo-cd' && github.event_name != 'push' }} uses: ./.github/workflows/image-reuse.yaml with: # Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations) @@ -63,56 +77,59 @@ jobs: contents: read packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags id-token: write # for creating OIDC tokens for signing. - if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }} + if: ${{ github.repository == 'Skyscanner/argo-cd' && github.event_name == 'push' }} uses: ./.github/workflows/image-reuse.yaml with: - quay_image_name: quay.io/argoproj/argocd:latest - ghcr_image_name: ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} + # quay_image_name: quay.io/argoproj/argocd:latest + ghcr_image_name: ghcr.io/skyscanner/argocd:${{ needs.set-vars.outputs.image-tag }} # Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations) # renovate: datasource=golang-version packageName=golang go-version: 1.24.4 platforms: ${{ needs.set-vars.outputs.platforms }} push: true secrets: - quay_username: ${{ secrets.RELEASE_QUAY_USERNAME }} - quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }} + #quay_username: ${{ secrets.RELEASE_QUAY_USERNAME }} + #quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }} ghcr_username: ${{ github.actor }} ghcr_password: ${{ secrets.GITHUB_TOKEN }} - build-and-publish-provenance: # Push attestations to GHCR, latest image is polluting quay.io - needs: - - build-and-publish - permissions: - actions: read # for detecting the Github Actions environment. - id-token: write # for creating OIDC tokens for signing. - packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues) - if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }} - # Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 - with: - image: ghcr.io/argoproj/argo-cd/argocd - digest: ${{ needs.build-and-publish.outputs.image-digest }} - registry-username: ${{ github.actor }} - secrets: - registry-password: ${{ secrets.GITHUB_TOKEN }} + # TODO: Needs allowlisting + # build-and-publish-provenance: # Push attestations to GHCR, latest image is polluting quay.io + # needs: + # - build-and-publish + # permissions: + # actions: read # for detecting the Github Actions environment. + # id-token: write # for creating OIDC tokens for signing. + # packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues) + # if: ${{ github.repository == 'Skyscanner/argo-cd' && github.event_name == 'push' }} + # # Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator + # uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 + # with: + # image: ghcr.io/argoproj/argo-cd/argocd + # digest: ${{ needs.build-and-publish.outputs.image-digest }} + # registry-username: ${{ github.actor }} + # secrets: + # registry-password: ${{ secrets.GITHUB_TOKEN }} - Deploy: - needs: - - build-and-publish - - set-vars - permissions: - contents: write # for git to push upgrade commit if not already deployed - packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags - if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }} - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - - run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments" - env: - TOKEN: ${{ secrets.TOKEN }} - - run: | - docker run -u $(id -u):$(id -g) -v $(pwd):/src -w /src --rm -t ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} kustomize edit set image quay.io/argoproj/argocd=ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} - git config --global user.email 'ci@argoproj.com' - git config --global user.name 'CI' - git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ needs.set-vars.outputs.image-tag }}' && git push) - working-directory: argoproj-deployments/argocd + # TODO: We would need to fork this repo as well and repoint it to our argocd-deployments repo. + # Do we want this? + # Deploy: + # needs: + # - build-and-publish + # - set-vars + # permissions: + # contents: write # for git to push upgrade commit if not already deployed + # packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags + # if: ${{ github.repository == 'Skyscanner/argo-cd' && github.event_name == 'push' }} + # runs-on: ubuntu-22.04 + # steps: + # - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 + # - run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments" + # env: + # TOKEN: ${{ secrets.TOKEN }} + # - run: | + # docker run -u $(id -u):$(id -g) -v $(pwd):/src -w /src --rm -t ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} kustomize edit set image quay.io/argoproj/argocd=ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} + # git config --global user.email 'ci@argoproj.com' + # git config --global user.name 'CI' + # git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ needs.set-vars.outputs.image-tag }}' && git push) + # working-directory: argoproj-deployments/argocd diff --git a/.github/workflows/init-fix-branch.yaml b/.github/workflows/init-fix-branch.yaml new file mode 100644 index 0000000000000..41744c7141333 --- /dev/null +++ b/.github/workflows/init-fix-branch.yaml @@ -0,0 +1,37 @@ +name: Set up fix branch + +on: + workflow_dispatch: + inputs: + release_tag: + description: 'Exact release tag to base off (e.g. v2.14.9)' + required: true + type: string + fix_suffix: + description: 'Short name for this fix branch (e.g. fix-issue-123)' + required: true + type: string + +permissions: + contents: write + +jobs: + setup: + runs-on: ubuntu-latest + + steps: + - name: Checkout full repo (branches & tags) + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.18' + + - name: Run setup-fix + run: | + go run tools/fork-cli/main.go setup-fix \ + --release="${{ inputs.release_tag }}" \ + --fix-suffix="${{ inputs.fix_suffix }}" \ No newline at end of file diff --git a/.github/workflows/init-release.yaml b/.github/workflows/init-release.yaml deleted file mode 100644 index 2dc750d6888ca..0000000000000 --- a/.github/workflows/init-release.yaml +++ /dev/null @@ -1,77 +0,0 @@ -name: Init ArgoCD Release -on: - workflow_dispatch: - inputs: - TARGET_BRANCH: - description: 'TARGET_BRANCH to checkout (e.g. release-2.5)' - required: true - type: string - - TARGET_VERSION: - description: 'TARGET_VERSION to build manifests (e.g. 2.5.0-rc1) Note: the `v` prefix is not used' - required: true - type: string - -permissions: {} - -jobs: - prepare-release: - permissions: - contents: write # for peter-evans/create-pull-request to create branch - pull-requests: write # for peter-evans/create-pull-request to create a PR - name: Automatically generate version and manifests on ${{ inputs.TARGET_BRANCH }} - runs-on: ubuntu-22.04 - steps: - - name: Checkout code - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ inputs.TARGET_BRANCH }} - - - name: Check if TARGET_VERSION is well formed. - run: | - set -xue - # Target version must not contain 'v' prefix - if echo "${{ inputs.TARGET_VERSION }}" | grep -e '^v'; then - echo "::error::Target version '${{ inputs.TARGET_VERSION }}' should not begin with a 'v' prefix, refusing to continue." >&2 - exit 1 - fi - - - name: Create VERSION information - run: | - set -ue - echo "Bumping version from $(cat VERSION) to ${{ inputs.TARGET_VERSION }}" - echo "${{ inputs.TARGET_VERSION }}" > VERSION - - # We install kustomize in the dist directory - - name: Add dist to PATH - run: | - echo "/home/runner/work/argo-cd/argo-cd/dist" >> $GITHUB_PATH - - - name: Generate new set of manifests - run: | - set -ue - make install-codegen-tools-local - make manifests-local VERSION=${{ inputs.TARGET_VERSION }} - git diff - - - name: Generate version compatibility table - run: | - git stash - bash hack/update-supported-versions.sh - git add -u . - git stash pop - - - name: Create pull request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 - with: - commit-message: "Bump version to ${{ inputs.TARGET_VERSION }}" - title: "Bump version to ${{ inputs.TARGET_VERSION }} on ${{ inputs.TARGET_BRANCH }} branch" - body: Updating VERSION and manifests to ${{ inputs.TARGET_VERSION }} - branch: update-version - branch-suffix: random - signoff: true - labels: release - - diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml deleted file mode 100644 index 81c19819237aa..0000000000000 --- a/.github/workflows/pr-title-check.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: "Lint PR" - -on: - pull_request_target: - types: [opened, edited, reopened, synchronize] - -# IMPORTANT: No checkout actions, scripts, or builds should be added to this workflow. Permissions should always be used -# with extreme caution. https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target -permissions: {} - -# PR updates can happen in quick succession leading to this -# workflow being trigger a number of times. This limits it -# to one run per PR. -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref }} - cancel-in-progress: true - -jobs: - validate: - permissions: - contents: read - pull-requests: read - name: Validate PR Title - runs-on: ubuntu-latest - steps: - - uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - configuration_path: ".github/pr-title-checker-config.json" diff --git a/.github/workflows/promote-fix-branch.yaml b/.github/workflows/promote-fix-branch.yaml new file mode 100644 index 0000000000000..437a4d971b9bd --- /dev/null +++ b/.github/workflows/promote-fix-branch.yaml @@ -0,0 +1,37 @@ +name: Promote Fix + +on: + workflow_dispatch: + inputs: + fix_branch: + description: 'Full fix branch (e.g. skyscanner-internal/releases/v2.14.9/fix-issue-123)' + required: true + type: string + proposal_branch: + description: 'Name under skyscanner-contrib/proposal/ (e.g. fix-issue-123)' + required: true + type: string + +permissions: + contents: write + +jobs: + promote: + runs-on: ubuntu-latest + + steps: + - name: Checkout full repo (branches & tags) + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.24.3' + + - name: Invoke promote-fix + run: | + go run tools/fork-cli/main.go promote-fix \ + --fix-branch="${{ inputs.fix_branch }}" \ + --proposal-branch="${{ inputs.proposal_branch }}" diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml deleted file mode 100644 index 76913e1fec120..0000000000000 --- a/.github/workflows/scorecard.yaml +++ /dev/null @@ -1,67 +0,0 @@ -name: Scorecards supply-chain security -on: - # Only the default branch is supported. - branch_protection_rule: - schedule: - - cron: "39 9 * * 2" - push: - branches: ["master"] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Declare default permissions as read only. -permissions: read-all - -jobs: - analysis: - name: Scorecards analysis - runs-on: ubuntu-22.04 - permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Used to receive a badge. (Upcoming feature) - id-token: write - # Needs for private repositories. - contents: read - actions: read - if: github.repository == 'argoproj/argo-cd' - - steps: - - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - with: - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 - with: - results_file: results.sarif - results_format: sarif - # (Optional) Read-only PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecards on a *private* repository - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. - # repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} - - # Publish the results for public repositories to enable scorecard badges. For more details, see - # https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories, `publish_results` will automatically be set to `false`, regardless - # of the value entered here. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard. - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8fcfedf57053e09257688fce7a0beeb18b1b9ae3 # v2.17.2 - with: - sarif_file: results.sarif diff --git a/.github/workflows/update-snyk.yaml b/.github/workflows/update-snyk.yaml deleted file mode 100644 index b4d98134e84ad..0000000000000 --- a/.github/workflows/update-snyk.yaml +++ /dev/null @@ -1,36 +0,0 @@ -name: Snyk report update -on: - workflow_dispatch: {} - schedule: - - cron: '0 0 * * 0' # midnight every Sunday - -permissions: - contents: read - -jobs: - snyk-report: - permissions: - contents: write - pull-requests: write - if: github.repository == 'argoproj/argo-cd' - name: Update Snyk report in the docs directory - runs-on: ubuntu-22.04 - steps: - - name: Checkout code - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - name: Build reports - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - run: | - make snyk-report - pr_branch="snyk-update-$(echo $RANDOM | md5sum | head -c 20)" - git checkout -b "$pr_branch" - git config --global user.email 'ci@argoproj.com' - git config --global user.name 'CI' - git add docs/snyk - git commit -m "[Bot] docs: Update Snyk reports" --signoff - git push --set-upstream origin "$pr_branch" - gh pr create -B master -H "$pr_branch" --title '[Bot] docs: Update Snyk report' --body '' diff --git a/Dockerfile b/Dockerfile index 453b01acdba53..027da285b27b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ RUN ./install.sh helm && \ #################################################################################################### FROM $BASE_IMAGE AS argocd-base -LABEL org.opencontainers.image.source="https://github.com/argoproj/argo-cd" +LABEL org.opencontainers.image.source="https://github.com/Skyscanner/argo-cd" USER root @@ -49,7 +49,8 @@ RUN groupadd -g $ARGOCD_USER_ID argocd && \ chown argocd:0 /home/argocd && \ chmod g=u /home/argocd && \ apt-get update && \ - apt-get dist-upgrade -y && \ + # TODO: This breaks our GHEC and GHES runners for some reason + # apt-get dist-upgrade -y && \ apt-get install -y \ git git-lfs tini gpg tzdata connect-proxy && \ apt-get clean && \ diff --git a/tools/fork-cli/INTERNAL_CONTRIB.md b/tools/fork-cli/INTERNAL_CONTRIB.md new file mode 100644 index 0000000000000..ed2953c6d5579 --- /dev/null +++ b/tools/fork-cli/INTERNAL_CONTRIB.md @@ -0,0 +1,209 @@ +# Fork CLI: ArgoCD Internal Contribution Guide + +This guide explains how to work with our ArgoCD fork at Skyscanner. There are two main workflows: + +1. **Internal Development** - Developing fixes/features for internal use at Skyscanner +2. **Upstream Contribution** - Contributing our changes back to ArgoCD upstream + +## Branch Structure + +Our fork maintains several important branches: + +- **`skyscanner-internal/master`** + - Default branch of our fork (`skyscanner/argo-cd`) + - Contains latest upstream code + Skyscanner CI files + - Should contain minimal changes (mainly CI/build configs and tooling) + - Updated by rebasing onto `skyscanner-contrib/master` + +- **`skyscanner-internal/develop//`** + - Release-pinned development branches based on upstream tags + - Format: `skyscanner-internal/develop/vX.Y.Z/fix-` + - Created by the **setup-fix** tool + - Contains our CI folder copied from `skyscanner-internal/master` and fork tooling + +- **`skyscanner-contrib/master`** + - Mirror of `argoproj/argo-cd:master` + - Kept in sync via automation + - Never push to this directly + +- **`skyscanner-contrib/proposal/`** + - "Clean" branch for upstream proposals + - Contains only the commits we want to contribute upstream + - Created by the **promote-fix** tool + +## Internal Development Workflow + +### Before Starting Development + +1. **Ensure clean working directory**: Commit or stash any local changes + ```shell + git status + # If you have changes: + git stash push -m "WIP before sync" + # or + git add . && git commit -m "WIP: save local changes" + ``` + +2. **Sync our fork with upstream**: + ```shell + go run tools/fork-cli/main.go sync-fork + ``` + This ensures both `skyscanner-contrib/master` and `skyscanner-internal/master` are up to date. + +3. **Check if the issue/feature already exists**: + - Verify latest ArgoCD releases at https://github.com/argoproj/argo-cd/releases + - Check ArgoCD issue tracker: https://github.com/argoproj/argo-cd/issues + - Consider coordinating with upstream maintainers for major changes + +### Starting Development + +1. **Set up a fix branch** based on the release tag we're using: + ```shell + go run tools/fork-cli/main.go setup-fix --release="vX.Y.Z" --fix-suffix="fix-name" + ``` + This creates `skyscanner-internal/develop/vX.Y.Z/fix-name` + + **Note**: The tool will fail if: + - You have uncommitted changes (stash them first) + - The release tag doesn't exist (run `git fetch --tags` or `sync-fork`) + - Your local branches are out of date (run `sync-fork`) + +2. **Create feature branches** from the development branch: + ```shell + go run tools/fork-cli/main.go work-on \ + --dev-branch="skyscanner-internal/develop/vX.Y.Z/fix-name" \ + --suffix="your-feature" + ``` + This creates `feature/your-feature` and provides instructions for creating PRs. + +3. **Make changes and create PRs**: + - Make changes, commit, and push your feature branch + - Create PRs against the development branch (not upstream!) + - Use the `gh` CLI for convenience: + ```shell + gh pr create --base skyscanner-internal/develop/vX.Y.Z/fix-name \ + --title "Your feature title" \ + --body "Description of changes" + ``` + +4. **Internal CI** will build and publish the image to GHCR for Skyscanner use. + +## Upstream Contribution Workflow + +1. **Promote your fix** to a proposal branch: + ```shell + go run tools/fork-cli/main.go promote-fix \ + --fix-branch="skyscanner-internal/develop/vX.Y.Z/fix-name" \ + --proposal-branch="proposal-name" + ``` + + **Note**: This tool will fail if: + - Your `skyscanner-contrib/master` is not up to date (run `sync-fork` first) + - The tool only creates the proposal branch now - it no longer touches `skyscanner-internal/master` + +2. **Handle conflicts** during promotion: + - The tool will guide you through resolving conflicts locally + - In CI mode, it will tell you to run the command locally + - Follow instructions to fix conflicts and continue + +3. **Create a PR to upstream ArgoCD**: + - Push the proposal branch: `git push origin skyscanner-contrib/proposal/proposal-name` + - Create PR to `argoproj/argo-cd:master` from the proposal branch + - Reference the related issue number (e.g., "Fixes #123") + - Provide clear description of changes + +4. **After upstream merge**: + - Remove proposal branch + - Continue using internal tag until upstream releases a version with your fix + - Plan to migrate to the official release once available + +## Command Reference + +### sync-fork +Synchronizes both fork branches with upstream. **Run this regularly!** + +```shell +go run tools/fork-cli/main.go sync-fork +``` + +**Prerequisites**: Clean working directory (no uncommitted changes) + +### setup-fix +Creates a release-pinned development branch with CI tools imported. + +```shell +go run tools/fork-cli/main.go setup-fix --release="v2.14.9" --fix-suffix="fix-issue-123" +``` + +**Prerequisites**: +- Clean working directory +- Release tag exists +- Local branches up to date (run `sync-fork` if needed) + +### work-on +Creates a feature branch off a development branch and provides PR creation guidance. + +```shell +go run tools/fork-cli/main.go work-on \ + --dev-branch="skyscanner-internal/develop/v2.14.9/fix-issue-123" \ + --suffix="add-logging" +``` + +### promote-fix +Creates a clean proposal branch for upstream contribution. + +```shell +go run tools/fork-cli/main.go promote-fix \ + --fix-branch="skyscanner-internal/develop/v2.14.9/fix-issue-123" \ + --proposal-branch="fix-issue-123" +``` + +**Prerequisites**: +- `skyscanner-contrib/master` up to date (run `sync-fork` if needed) + +## Best Practices + +- **Always start with sync-fork** to ensure you're working with the latest code +- **Keep working directory clean** - the tools check for uncommitted changes +- **Always start from a release tag** that we're currently using +- **Verify upstream first** - check if the issue is already fixed in newer versions +- **Coordinate with upstream** before major features or changes +- **Keep internal changes minimal** - aim to contribute everything back +- **Handle conflicts locally** using the fork-cli tools +- **Use the internal build** only until upstream absorbs the change + +## Troubleshooting + +### "You have uncommitted changes" +```shell +# Either commit your changes: +git add . && git commit -m "WIP: description" + +# Or stash them: +git stash push -m "WIP before running fork-cli" +# Later restore with: git stash pop +``` + +### "Tag 'vX.Y.Z' does not exist" +```shell +# Fetch latest tags: +git fetch --tags + +# Or sync everything: +go run tools/fork-cli/main.go sync-fork +``` + +### "Branch is not up to date" +```shell +# Sync all branches: +go run tools/fork-cli/main.go sync-fork +``` + +### Conflicts during promotion +The tool will leave you in a cherry-pick state. Resolve conflicts manually: +```shell +# Fix conflicts in your editor, then: +git add . +git cherry-pick --continue +# Then re-run the promote-fix command +``` \ No newline at end of file diff --git a/tools/fork-cli/main.go b/tools/fork-cli/main.go new file mode 100644 index 0000000000000..9a2ed9df39f55 --- /dev/null +++ b/tools/fork-cli/main.go @@ -0,0 +1,536 @@ +// Four subcommands: +// β€’ setup-fix (requires --release, --fix-suffix) +// β€’ promote-fix (requires --fix-branch, --proposal-branch) +// β€’ sync-fork (syncs both forks with upstream) +// β€’ work-on (requires --dev-branch, --suffix) +// +// In CI mode (GITHUB_ACTIONS=true), any conflict aborts and prints a short +// "run locally" instruction. Locally, conflicts leave you inside a cherry-pick for +// manual resolution. + +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +// CommandRunner abstracts command execution for better testability +type CommandRunner interface { + // Run executes a command with output to stdout/stderr + Run(cmdName string, args ...string) error + // RunOrExit executes a command and exits on failure + RunOrExit(cmdName string, args ...string) + // RunWithOutput executes a command and returns its stdout output + RunWithOutput(cmdName string, args ...string) (string, error) + // RunAndCaptureOrExit executes a command, captures its output, exits on error + RunAndCaptureOrExit(cmdName string, args ...string) string + // BranchExists checks if a git branch exists + BranchExists(ref string) bool + // TagExists checks if a git tag exists + TagExists(tag string) bool + // IsCI checks if running in CI environment + IsCI() bool + // ExitWithError prints error message and exits + ExitWithError(format string, args ...any) + // HasUncommittedChanges checks if working directory is dirty + HasUncommittedChanges() bool + // IsBranchUpToDate checks if local branch is up to date with remote + IsBranchUpToDate(localBranch, remoteBranch string) bool +} + +// DefaultRunner is the standard command runner used in production +type DefaultRunner struct { + stdout io.Writer + stderr io.Writer + exitFn func(int) + env map[string]string +} + +// NewDefaultRunner creates a runner with standard configuration +func NewDefaultRunner() *DefaultRunner { + return &DefaultRunner{ + stdout: os.Stdout, + stderr: os.Stderr, + exitFn: os.Exit, + env: envToMap(), + } +} + +func envToMap() map[string]string { + result := make(map[string]string) + for _, env := range os.Environ() { + parts := strings.SplitN(env, "=", 2) + if len(parts) == 2 { + result[parts[0]] = parts[1] + } + } + return result +} + +func (r *DefaultRunner) IsCI() bool { + return r.env["GITHUB_ACTIONS"] == "true" +} + +func (r *DefaultRunner) Run(cmdName string, args ...string) error { + cmd := exec.Command(cmdName, args...) + cmd.Stdout = r.stdout + cmd.Stderr = r.stderr + return cmd.Run() +} + +func (r *DefaultRunner) RunOrExit(cmdName string, args ...string) { + if err := r.Run(cmdName, args...); err != nil { + fmt.Fprintf(r.stderr, "ERROR: command failed: %s %s\n", cmdName, strings.Join(args, " ")) + r.exitFn(1) + } +} + +func (r *DefaultRunner) RunWithOutput(cmdName string, args ...string) (string, error) { + var stdout bytes.Buffer + cmd := exec.Command(cmdName, args...) + cmd.Stdout = &stdout + cmd.Stderr = r.stderr + err := cmd.Run() + return strings.TrimSpace(stdout.String()), err +} + +func (r *DefaultRunner) RunAndCaptureOrExit(cmdName string, args ...string) string { + output, err := r.RunWithOutput(cmdName, args...) + if err != nil { + fmt.Fprintf(r.stderr, "ERROR: '%s %s' failed\n", cmdName, strings.Join(args, " ")) + r.exitFn(1) + } + return output +} + +func (r *DefaultRunner) BranchExists(ref string) bool { + err := exec.Command("git", "rev-parse", "--verify", "--quiet", ref).Run() + return err == nil +} + +func (r *DefaultRunner) TagExists(tag string) bool { + err := exec.Command("git", "rev-parse", "--verify", "--quiet", "tags/"+tag).Run() + return err == nil +} + +func (r *DefaultRunner) HasUncommittedChanges() bool { + // Check if working directory is clean + output, err := r.RunWithOutput("git", "status", "--porcelain") + if err != nil { + return true // assume dirty on error + } + return strings.TrimSpace(output) != "" +} + +func (r *DefaultRunner) IsBranchUpToDate(localBranch, remoteBranch string) bool { + // Get the commit hash of local branch + localHash, err := r.RunWithOutput("git", "rev-parse", localBranch) + if err != nil { + return false + } + + // Get the commit hash of remote branch + remoteHash, err := r.RunWithOutput("git", "rev-parse", remoteBranch) + if err != nil { + return false + } + + return localHash == remoteHash +} + +func (r *DefaultRunner) ExitWithError(format string, args ...any) { + fmt.Fprintf(r.stderr, format+"\n", args...) + r.exitFn(1) +} + +// Core CLI application +func main() { + runner := NewDefaultRunner() + exitCode := run(os.Args, runner) + os.Exit(exitCode) +} + +func run(args []string, runner CommandRunner) int { + if len(args) < 2 { + usage(os.Stderr) + return 1 + } + + switch args[1] { + case "setup-fix": + return setupFixCmd(args[2:], runner) + case "promote-fix": + return promoteFixCmd(args[2:], runner) + case "sync-fork": + return syncForkCmd(args[2:], runner) + case "work-on": + return workOnCmd(args[2:], runner) + default: + usage(os.Stderr) + return 1 + } +} + +func usage(w io.Writer) { + fmt.Fprintf(w, ` +Usage: + cli setup-fix --release= --fix-suffix= + cli promote-fix --fix-branch= --proposal-branch= + cli sync-fork + cli work-on --dev-branch= --suffix= + +Environment: + β€’ Set GITHUB_ACTIONS=true for CI mode. On conflict in CI, the CLI prints a + one-liner telling you to run it locally, then exits 1. + β€’ Locally (GITHUB_ACTIONS unset), conflicts leave you inside a cherry-pick for + manual resolution. + +Commands: + + setup-fix + β€’ --release exact tag to base off (e.g. v2.9.14) + β€’ --fix-suffix short name (e.g. fix-issue-123) + + promote-fix + β€’ --fix-branch must be "skyscanner-internal/develop//" + β€’ --proposal-branch short name under skyscanner-contrib/proposal/ (e.g. fix-issue-123) + + sync-fork + β€’ Syncs both forks with upstream GitHub repository (argoproj/argo-cd) + β€’ Rebases skyscanner-contrib/master on top of upstream/master + β€’ Rebases skyscanner-internal/master on top of skyscanner-contrib/master + + work-on + β€’ --dev-branch release-pinned dev branch (e.g. skyscanner-internal/develop/v2.14.9/fix-issue-123) + β€’ --suffix short name for feature branch (e.g. add-logging) +`) +} + +// setupFixCmd: rebase internal/master β†’ create release branch β†’ import CI β†’ push. +func setupFixCmd(args []string, runner CommandRunner) int { + fs := flag.NewFlagSet("setup-fix", flag.ExitOnError) + release := fs.String("release", "", "exact tag to base off (e.g. v2.9.14)") + fixSuffix := fs.String("fix-suffix", "", "short name for fix branch (e.g. fix-issue-123)") + if err := fs.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + return 1 + } + + if *release == "" || *fixSuffix == "" { + fs.Usage() + return 1 + } + + // Check for uncommitted changes + if runner.HasUncommittedChanges() { + fmt.Fprintln(os.Stderr, "❌ You have uncommitted changes. Please commit or stash them before running setup-fix.") + fmt.Fprintln(os.Stderr, " git stash push -m \"WIP before setup-fix\"") + fmt.Fprintln(os.Stderr, " # or commit your changes") + return 1 + } + + // Check if the tag exists + if !runner.TagExists(*release) { + fmt.Fprintf(os.Stderr, "❌ Tag '%s' does not exist. Please fetch latest tags:\n", *release) + fmt.Fprintln(os.Stderr, " git fetch --tags") + fmt.Fprintln(os.Stderr, " # or run sync-fork to update everything") + return 1 + } + + // 1) Fetch remotes + runner.RunOrExit("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + + // Check if skyscanner-internal/master is up to date + if !runner.IsBranchUpToDate("skyscanner-internal/master", "origin/skyscanner-internal/master") { + fmt.Fprintln(os.Stderr, "❌ Your local skyscanner-internal/master is not up to date with origin.") + fmt.Fprintln(os.Stderr, " Please run sync-fork first to update all branches:") + fmt.Fprintln(os.Stderr, " go run tools/fork-cli/main.go sync-fork") + return 1 + } + + // 2) Rebase internal/master onto contrib/master + runner.RunOrExit("git", "checkout", "skyscanner-internal/master") + if err := runner.Run("git", "rebase", "skyscanner-contrib/master"); err != nil { + fmt.Fprintln(os.Stderr, "ERROR: rebase conflict. Resolve manually, then `git rebase --continue`.") + return 1 + } + runner.RunOrExit("git", "push", "--force", "origin", "skyscanner-internal/master") + + // 3) Create release‐based branch + newBranch := fmt.Sprintf("skyscanner-internal/develop/%s/%s", *release, *fixSuffix) + runner.RunOrExit("git", "checkout", "tags/"+*release, "-b", newBranch) + + // 4) Import .github/ and tools/fork-cli from internal/master + runner.RunOrExit("git", "checkout", "skyscanner-internal/master", "--", ".github") + runner.RunOrExit("git", "checkout", "skyscanner-internal/master", "--", "tools/fork-cli") + runner.RunOrExit("git", "commit", "-m", "chore: import CI and fork-cli tools into "+newBranch) + + // 5) Push new branch + runner.RunOrExit("git", "push", "-u", "origin", newBranch) + + fmt.Printf("βœ… Created branch %s\n", newBranch) + return 0 +} + +// promoteFixCmd: create proposal branch only, don't touch internal/master. +func promoteFixCmd(args []string, runner CommandRunner) int { + fs := flag.NewFlagSet("promote-fix", flag.ExitOnError) + fixBranch := fs.String("fix-branch", "", "e.g. skyscanner-internal/develop/v2.9.14/fix-issue-123") + proposal := fs.String("proposal-branch", "", "e.g. fix-issue-123)") + if err := fs.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + return 1 + } + + if *fixBranch == "" || *proposal == "" { + fs.Usage() + return 1 + } + + ciMode := runner.IsCI() + + // 1) Extract release tag from fixBranch + parts := strings.Split(*fixBranch, "/") + if len(parts) < 4 || parts[0] != "skyscanner-internal" || parts[1] != "develop" { + fmt.Fprintf(os.Stderr, "ERROR: fix-branch must be 'skyscanner-internal/develop//'. Got '%s'\n", *fixBranch) + return 1 + } + + // 2) Fetch necessary refs + runner.RunOrExit("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + + // Check if skyscanner-contrib/master is up to date + if !runner.IsBranchUpToDate("skyscanner-contrib/master", "origin/skyscanner-contrib/master") { + fmt.Fprintln(os.Stderr, "❌ Your local skyscanner-contrib/master is not up to date with origin.") + fmt.Fprintln(os.Stderr, " Please commit/stash local changes and run sync-fork first:") + fmt.Fprintln(os.Stderr, " git stash push -m \"WIP before sync-fork\"") + fmt.Fprintln(os.Stderr, " go run tools/fork-cli/main.go sync-fork") + return 1 + } + + baseRelease := parts[2] // e.g. v2.9.14 + + // 3) Compute merge-base & commit-range + gb := strings.TrimSpace(runner.RunAndCaptureOrExit("git", "merge-base", *fixBranch, baseRelease)) + if gb == "" { + fmt.Fprintf(os.Stderr, "ERROR: cannot find merge-base between %s and %s\n", *fixBranch, baseRelease) + return 1 + } + commitRange := fmt.Sprintf("%s..%s", gb, *fixBranch) + fmt.Printf("Cherry-pick range: %s\n\n", commitRange) + + // Cherry-pick into skyscanner-contrib/proposal/ + proposalFull := "skyscanner-contrib/proposal/" + *proposal + fmt.Printf("β†’ Creating/updating %s off skyscanner-contrib/master …\n", proposalFull) + runner.RunOrExit("git", "checkout", "skyscanner-contrib/master") + if runner.BranchExists(proposalFull) { + runner.RunOrExit("git", "branch", "-D", proposalFull) + } + runner.RunOrExit("git", "checkout", "-b", proposalFull) + + fmt.Printf("β†’ Cherry-picking into %s …\n", proposalFull) + err := runner.Run("git", "cherry-pick", "--keep-redundant-commits", commitRange) + if err != nil { + if ciMode { + printConflictMessage(os.Stderr, baseRelease, *fixBranch, *proposal) + return 1 + } + fmt.Fprintln(os.Stderr, "⚠️ Conflict detected in proposal branch. Resolve manually and then:") + fmt.Fprintln(os.Stderr, " git cherry-pick --continue") + return 1 + } + fmt.Printf("βœ… %s is ready for upstream contribution\n\n", proposalFull) + fmt.Println("Next steps:") + fmt.Printf(" git push origin %s\n", proposalFull) + fmt.Println(" # Then create a PR to argoproj/argo-cd:master from this branch") + return 0 +} + +// syncForkCmd: Sync forks by rebasing branches on top of their upstream counterparts. +func syncForkCmd(args []string, runner CommandRunner) int { + fs := flag.NewFlagSet("sync-fork", flag.ExitOnError) + if err := fs.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + return 1 + } + + // Check for uncommitted changes first + if runner.HasUncommittedChanges() { + fmt.Fprintln(os.Stderr, "❌ You have uncommitted changes. Please commit or stash them before syncing:") + fmt.Fprintln(os.Stderr, " git stash push -m \"WIP before sync-fork\"") + fmt.Fprintln(os.Stderr, " # or commit your changes") + fmt.Fprintln(os.Stderr, " # then re-run: go run tools/fork-cli/main.go sync-fork") + return 1 + } + + ciMode := runner.IsCI() + + // 1) Ensure upstream remote exists + fmt.Println("β†’ Ensuring upstream remote exists...") + remoteOutput, _ := runner.RunWithOutput("git", "remote") + remotes := strings.Fields(remoteOutput) + + hasUpstream := false + for _, remote := range remotes { + if remote == "upstream" { + hasUpstream = true + break + } + } + + if !hasUpstream { + fmt.Println("β†’ Adding upstream remote: https://github.com/argoproj/argo-cd.git") + runner.RunOrExit("git", "remote", "add", "upstream", "https://github.com/argoproj/argo-cd.git") + } + + // 2) Fetch from all remotes to ensure we have latest code + fmt.Println("β†’ Fetching latest changes from all remotes...") + runner.RunOrExit("git", "fetch", "--all") + + // 3) Handle contrib fork: rebase on top of upstream + contribBranch := "skyscanner-contrib/master" + upstreamBranch := "upstream/master" + + fmt.Printf("β†’ Rebasing %s on top of %s...\n", contribBranch, upstreamBranch) + + // Checkout contrib branch + runner.RunOrExit("git", "checkout", contribBranch) + + // Attempt to rebase on upstream + err := runner.Run("git", "rebase", upstreamBranch) + if err != nil { + if ciMode { + fmt.Fprintf(os.Stderr, "❌ Rebase conflict between %s and %s!\n", contribBranch, upstreamBranch) + fmt.Fprintln(os.Stderr, "Please run this command locally to resolve conflicts:") + fmt.Fprintf(os.Stderr, " go run tools/fork-cli/main.go sync-fork\n") + return 1 + } + fmt.Fprintf(os.Stderr, "⚠️ Conflict detected rebasing %s on %s.\n", contribBranch, upstreamBranch) + fmt.Fprintln(os.Stderr, "Resolve conflicts, then run:") + fmt.Fprintln(os.Stderr, " git rebase --continue") + fmt.Fprintln(os.Stderr, "Once finished, re-run this command to proceed with the internal fork.") + return 1 + } + + // Push the rebased contrib branch + fmt.Printf("β†’ Pushing rebased %s...\n", contribBranch) + runner.RunOrExit("git", "push", "--force", "origin", contribBranch) + + // 4) Handle the internal fork: rebase on top of contrib + internalBranch := "skyscanner-internal/master" + fmt.Printf("β†’ Rebasing %s on top of %s...\n", internalBranch, contribBranch) + + // Checkout internal branch + runner.RunOrExit("git", "checkout", internalBranch) + + // Attempt to rebase on contrib + err = runner.Run("git", "rebase", contribBranch) + if err != nil { + if ciMode { + fmt.Fprintf(os.Stderr, "❌ Rebase conflict between %s and %s!\n", internalBranch, contribBranch) + fmt.Fprintln(os.Stderr, "Please run this command locally to resolve conflicts:") + fmt.Fprintf(os.Stderr, " go run tools/fork-cli/main.go sync-fork\n") + return 1 + } + fmt.Fprintf(os.Stderr, "⚠️ Conflict detected rebasing %s on %s.\n", internalBranch, contribBranch) + fmt.Fprintln(os.Stderr, "Resolve conflicts, then run:") + fmt.Fprintln(os.Stderr, " git rebase --continue") + return 1 + } + + // Push the rebased internal branch + fmt.Printf("β†’ Pushing rebased %s...\n", internalBranch) + runner.RunOrExit("git", "push", "--force", "origin", internalBranch) + + fmt.Println("\nβœ… Fork synchronization complete!") + fmt.Printf("β€’ %s is rebased on %s\n", contribBranch, upstreamBranch) + fmt.Printf("β€’ %s is rebased on %s\n", internalBranch, contribBranch) + return 0 +} + +// workOnCmd: Create a feature branch and open PR against internal fork +func workOnCmd(args []string, runner CommandRunner) int { + fs := flag.NewFlagSet("work-on", flag.ExitOnError) + devBranch := fs.String("dev-branch", "", "release-pinned dev branch (e.g. skyscanner-internal/develop/v2.14.9/fix-issue-123)") + suffix := fs.String("suffix", "", "short name for feature branch (e.g. add-logging)") + if err := fs.Parse(args); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + return 1 + } + + if *devBranch == "" || *suffix == "" { + fs.Usage() + return 1 + } + + // Validate dev branch format + parts := strings.Split(*devBranch, "/") + if len(parts) < 4 || parts[0] != "skyscanner-internal" || parts[1] != "develop" { + fmt.Fprintf(os.Stderr, "ERROR: dev-branch must be 'skyscanner-internal/develop//'. Got '%s'\n", *devBranch) + return 1 + } + + // Create feature branch name + featureBranch := fmt.Sprintf("%s-%s", *devBranch, *suffix) + + // Create and checkout feature branch + runner.RunOrExit("git", "checkout", *devBranch) + runner.RunOrExit("git", "checkout", "-b", featureBranch) + + fmt.Printf("βœ… Created feature branch: %s\n", featureBranch) + fmt.Printf("β†’ Base branch: %s\n", *devBranch) + + // Read current VERSION file + currentVersion, err := runner.RunWithOutput("cat", "VERSION") + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Could not read VERSION file: %v\n", err) + return 1 + } + currentVersion = strings.TrimSpace(currentVersion) + + // Update VERSION file with suffix + newVersion := fmt.Sprintf("%s-%s", currentVersion, *suffix) + runner.RunOrExit("sh", "-c", fmt.Sprintf("echo '%s' > VERSION", newVersion)) + + // Commit the VERSION change + runner.RunOrExit("git", "add", "VERSION") + commitMsg := fmt.Sprintf("feat: %s\n\nUpdate VERSION to %s", *suffix, newVersion) + runner.RunOrExit("git", "commit", "-m", commitMsg) + + fmt.Printf("βœ… Updated VERSION from %s to %s\n", currentVersion, newVersion) + + fmt.Println("Setting up default repo and creating PR...") + + // Set default repo to fork (not upstream) + runner.RunOrExit("gh", "repo", "set-default", "Skyscanner/argo-cd") + + // Push the branch + runner.RunOrExit("git", "push", "-u", "origin", featureBranch) + + // Create PR automatically + title := fmt.Sprintf("feat: %s", *suffix) + body := fmt.Sprintf("Automated PR created via fork-cli\n\nUpdates VERSION to %s", newVersion) + runner.RunOrExit("gh", "pr", "create", "--base", *devBranch, "--title", title, "--body", body) + + fmt.Println("βœ… PR created successfully!") + fmt.Println() + fmt.Println("Next steps:") + fmt.Println("1. Make additional changes if needed") + fmt.Println("2. Commit and push any further changes") + + return 0 +} + +func printConflictMessage(w io.Writer, releaseTag, fixBranch, proposal string) { + cmd := fmt.Sprintf("go run tools/fork-cli/main.go promote-fix --fix-branch=\"%s\" --proposal-branch=\"%s\"", + fixBranch, proposal) + fmt.Fprintf(w, "\n❌ Conflict detected during promotion of release %s.\n", releaseTag) + fmt.Fprintln(w, "Please re-run this command locally to resolve interactively:") + fmt.Fprintf(w, " %s\n\n", cmd) +} diff --git a/tools/fork-cli/main_test.go b/tools/fork-cli/main_test.go new file mode 100644 index 0000000000000..8eac0c8c8430d --- /dev/null +++ b/tools/fork-cli/main_test.go @@ -0,0 +1,482 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// MockRunner implements CommandRunner for testing +type MockRunner struct { + t *testing.T + expectedCmds []expectedCmd + ciMode bool + exitCalls []string + branchesExist map[string]bool + tagsExist map[string]bool + output *bytes.Buffer + cmdIndex int + hasUncommittedChanges bool + branchUpToDate map[string]bool +} + +type expectedCmd struct { + command string + args []string + output string + err error +} + +func NewMockRunner(t *testing.T) *MockRunner { + t.Helper() + return &MockRunner{ + t: t, + expectedCmds: []expectedCmd{}, + branchesExist: make(map[string]bool), + tagsExist: make(map[string]bool), + output: &bytes.Buffer{}, + branchUpToDate: make(map[string]bool), + } +} + +func (m *MockRunner) ExpectCommand(command string, args ...string) *MockRunner { + m.expectedCmds = append(m.expectedCmds, expectedCmd{ + command: command, + args: args, + output: "", + err: nil, + }) + return m +} + +func (m *MockRunner) WithOutput(output string) *MockRunner { + lastIdx := len(m.expectedCmds) - 1 + if lastIdx >= 0 { + m.expectedCmds[lastIdx].output = output + } + return m +} + +func (m *MockRunner) WithError(err error) *MockRunner { + lastIdx := len(m.expectedCmds) - 1 + if lastIdx >= 0 { + m.expectedCmds[lastIdx].err = err + } + return m +} + +func (m *MockRunner) SetCIMode(enabled bool) *MockRunner { + m.ciMode = enabled + return m +} + +func (m *MockRunner) SetBranchExists(branch string, exists bool) *MockRunner { + m.branchesExist[branch] = exists + return m +} + +func (m *MockRunner) SetTagExists(tag string, exists bool) *MockRunner { + m.tagsExist[tag] = exists + return m +} + +func (m *MockRunner) SetHasUncommittedChanges(dirty bool) *MockRunner { + m.hasUncommittedChanges = dirty + return m +} + +func (m *MockRunner) SetBranchUpToDate(localBranch string, upToDate bool) *MockRunner { + m.branchUpToDate[localBranch] = upToDate + return m +} + +func (m *MockRunner) VerifyExpectations() { + if m.cmdIndex != len(m.expectedCmds) { + m.t.Errorf("Not all expected commands were executed. Expected %d, got %d", + len(m.expectedCmds), m.cmdIndex) + } +} + +func (m *MockRunner) Run(cmdName string, args ...string) error { + return m.checkCommand(cmdName, args, false) +} + +func (m *MockRunner) RunOrExit(cmdName string, args ...string) { + if err := m.checkCommand(cmdName, args, false); err != nil { + m.exitCalls = append(m.exitCalls, fmt.Sprintf("%s %s", cmdName, strings.Join(args, " "))) + } +} + +func (m *MockRunner) RunWithOutput(cmdName string, args ...string) (string, error) { + err := m.checkCommand(cmdName, args, true) + if m.cmdIndex-1 >= 0 && m.cmdIndex-1 < len(m.expectedCmds) { + return m.expectedCmds[m.cmdIndex-1].output, err + } + return "", err +} + +func (m *MockRunner) RunAndCaptureOrExit(cmdName string, args ...string) string { + output, err := m.RunWithOutput(cmdName, args...) + if err != nil { + m.exitCalls = append(m.exitCalls, fmt.Sprintf("%s %s", cmdName, strings.Join(args, " "))) + return "" + } + return output +} + +func (m *MockRunner) BranchExists(ref string) bool { + exists, ok := m.branchesExist[ref] + if !ok { + m.t.Logf("Branch existence check not mocked for: %s", ref) + return false + } + return exists +} + +func (m *MockRunner) TagExists(tag string) bool { + exists, ok := m.tagsExist[tag] + if !ok { + m.t.Logf("Tag existence check not mocked for: %s", tag) + return false + } + return exists +} + +func (m *MockRunner) HasUncommittedChanges() bool { + return m.hasUncommittedChanges +} + +func (m *MockRunner) IsBranchUpToDate(localBranch, remoteBranch string) bool { + upToDate, ok := m.branchUpToDate[localBranch] + if !ok { + m.t.Logf("Branch up-to-date check not mocked for: %s", localBranch) + return false + } + return upToDate +} + +func (m *MockRunner) IsCI() bool { + return m.ciMode +} + +func (m *MockRunner) ExitWithError(format string, args ...any) { + m.exitCalls = append(m.exitCalls, fmt.Sprintf(format, args...)) +} + +func (m *MockRunner) checkCommand(cmdName string, args []string, _ bool) error { + if m.cmdIndex >= len(m.expectedCmds) { + m.t.Errorf("Unexpected command: %s %s", cmdName, strings.Join(args, " ")) + return errors.New("unexpected command") + } + + expected := m.expectedCmds[m.cmdIndex] + m.cmdIndex++ + + assert.Equal(m.t, expected.command, cmdName, "Command name mismatch") + assert.Equal(m.t, expected.args, args, "Command args mismatch") + + return expected.err +} + +func TestSetupFixCmd(t *testing.T) { + t.Run("successful setup", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + mock.SetTagExists("v2.14.9", true) + mock.SetBranchUpToDate("skyscanner-internal/master", true) + + // Expect the git commands to be called in sequence + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-internal/master") + mock.ExpectCommand("git", "checkout", "tags/v2.14.9", "-b", "skyscanner-internal/develop/v2.14.9/fix-issue-123") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master", "--", ".github") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master", "--", "tools/fork-cli") + mock.ExpectCommand("git", "commit", "-m", "chore: import CI and fork-cli tools into skyscanner-internal/develop/v2.14.9/fix-issue-123") + mock.ExpectCommand("git", "push", "-u", "origin", "skyscanner-internal/develop/v2.14.9/fix-issue-123") + + exitCode := setupFixCmd([]string{"--release=v2.14.9", "--fix-suffix=fix-issue-123"}, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("fails with uncommitted changes", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(true) + + exitCode := setupFixCmd([]string{"--release=v2.14.9", "--fix-suffix=fix-issue-123"}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("fails with non-existent tag", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + mock.SetTagExists("v2.14.9", false) + + exitCode := setupFixCmd([]string{"--release=v2.14.9", "--fix-suffix=fix-issue-123"}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("fails with out-of-date internal master", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + mock.SetTagExists("v2.14.9", true) + mock.SetBranchUpToDate("skyscanner-internal/master", false) + + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + + exitCode := setupFixCmd([]string{"--release=v2.14.9", "--fix-suffix=fix-issue-123"}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("missing arguments", func(t *testing.T) { + mock := NewMockRunner(t) + exitCode := setupFixCmd([]string{"--release=v2.14.9"}, mock) // Missing fix-suffix + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("rebase conflict", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + mock.SetTagExists("v2.14.9", true) + mock.SetBranchUpToDate("skyscanner-internal/master", true) + + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master").WithError(errors.New("conflict")) + + exitCode := setupFixCmd([]string{"--release=v2.14.9", "--fix-suffix=fix-issue-123"}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) +} + +func TestPromoteFixCmd(t *testing.T) { + t.Run("successful promotion", func(t *testing.T) { + mock := NewMockRunner(t) + fixBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" + proposalBranch := "fix-issue-123" + proposalFull := "skyscanner-contrib/proposal/" + proposalBranch + + mock.SetBranchUpToDate("skyscanner-contrib/master", true) + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) + mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.SetBranchExists(proposalFull, false) + mock.ExpectCommand("git", "checkout", "-b", proposalFull) + mock.ExpectCommand("git", "cherry-pick", "--keep-redundant-commits", "abcdef123456.."+fixBranch) + + args := []string{"--fix-branch=" + fixBranch, "--proposal-branch=" + proposalBranch} + exitCode := promoteFixCmd(args, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("fails with out-of-date contrib master", func(t *testing.T) { + mock := NewMockRunner(t) + fixBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" + proposalBranch := "fix-issue-123" + + mock.SetBranchUpToDate("skyscanner-contrib/master", false) + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) + + args := []string{"--fix-branch=" + fixBranch, "--proposal-branch=" + proposalBranch} + exitCode := promoteFixCmd(args, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("conflict in CI mode", func(t *testing.T) { + mock := NewMockRunner(t).SetCIMode(true) + fixBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" + proposalBranch := "fix-issue-123" + proposalFull := "skyscanner-contrib/proposal/" + proposalBranch + + mock.SetBranchUpToDate("skyscanner-contrib/master", true) + mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") + mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) + mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.SetBranchExists(proposalFull, false) + mock.ExpectCommand("git", "checkout", "-b", proposalFull) + mock.ExpectCommand("git", "cherry-pick", "--keep-redundant-commits", "abcdef123456.."+fixBranch).WithError(errors.New("conflict")) + + args := []string{"--fix-branch=" + fixBranch, "--proposal-branch=" + proposalBranch} + exitCode := promoteFixCmd(args, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) +} + +func TestSyncForkCmd(t *testing.T) { + t.Run("successful sync with auth in CI mode", func(t *testing.T) { + mock := NewMockRunner(t).SetCIMode(true) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands for successful execution + mock.ExpectCommand("git", "remote").WithOutput("origin upstream") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-internal/master") + + // Set test environment + oldToken := os.Getenv("GITHUB_TOKEN") + os.Setenv("GITHUB_TOKEN", "test-token") + defer os.Setenv("GITHUB_TOKEN", oldToken) + + // Run the command + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("fails with uncommitted changes", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(true) + + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("successful sync with existing upstream", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands for successful execution + mock.ExpectCommand("git", "remote").WithOutput("origin upstream") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-internal/master") + + // Run the command + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("successful sync adding upstream", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands for successful execution with adding upstream + mock.ExpectCommand("git", "remote").WithOutput("origin") + mock.ExpectCommand("git", "remote", "add", "upstream", "https://github.com/argoproj/argo-cd.git") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-internal/master") + + // Run the command + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("conflict in contrib rebase", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands with a conflict during first rebase + mock.ExpectCommand("git", "remote").WithOutput("origin upstream") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master").WithError(errors.New("conflict")) + + // Run the command - should fail with exit code 1 + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("conflict in internal rebase", func(t *testing.T) { + mock := NewMockRunner(t) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands with conflict in second rebase + mock.ExpectCommand("git", "remote").WithOutput("origin upstream") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master") + mock.ExpectCommand("git", "push", "--force", "origin", "skyscanner-contrib/master") + mock.ExpectCommand("git", "checkout", "skyscanner-internal/master") + mock.ExpectCommand("git", "rebase", "skyscanner-contrib/master").WithError(errors.New("conflict")) + + // Run the command - should fail with exit code 1 + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("ci mode conflict handling", func(t *testing.T) { + mock := NewMockRunner(t).SetCIMode(true) + mock.SetHasUncommittedChanges(false) + + // Setup mock commands with conflict in CI mode + mock.ExpectCommand("git", "remote").WithOutput("origin upstream") + mock.ExpectCommand("git", "fetch", "--all") + mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") + mock.ExpectCommand("git", "rebase", "upstream/master").WithError(errors.New("conflict")) + + // Run the command - should fail with exit code 1 + exitCode := syncForkCmd([]string{}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) +} + +func TestWorkOnCmd(t *testing.T) { + t.Run("successful feature branch creation", func(t *testing.T) { + mock := NewMockRunner(t) + devBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" + suffix := "add-logging" + + mock.ExpectCommand("git", "fetch", "origin", devBranch+":"+devBranch) + mock.ExpectCommand("git", "checkout", devBranch) + mock.ExpectCommand("git", "checkout", "-b", "feature/add-logging") + + exitCode := workOnCmd([]string{"--dev-branch=" + devBranch, "--suffix=" + suffix}, mock) + assert.Equal(t, 0, exitCode) + mock.VerifyExpectations() + }) + + t.Run("invalid dev branch format", func(t *testing.T) { + mock := NewMockRunner(t) + devBranch := "invalid/branch/format" + suffix := "add-logging" + + exitCode := workOnCmd([]string{"--dev-branch=" + devBranch, "--suffix=" + suffix}, mock) + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) + + t.Run("missing arguments", func(t *testing.T) { + mock := NewMockRunner(t) + exitCode := workOnCmd([]string{"--dev-branch=skyscanner-internal/develop/v2.14.9/fix-issue-123"}, mock) // Missing suffix + assert.Equal(t, 1, exitCode) + mock.VerifyExpectations() + }) +} From 5938cd57a6505455779223a47dae1d9e1458a22b Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Tue, 1 Jul 2025 11:06:27 +0100 Subject: [PATCH 3/9] fix docs --- .github/workflows/init-fix-branch.yaml | 37 ----------------------- .github/workflows/promote-fix-branch.yaml | 37 ----------------------- tools/fork-cli/INTERNAL_CONTRIB.md | 5 ++- 3 files changed, 2 insertions(+), 77 deletions(-) delete mode 100644 .github/workflows/init-fix-branch.yaml delete mode 100644 .github/workflows/promote-fix-branch.yaml diff --git a/.github/workflows/init-fix-branch.yaml b/.github/workflows/init-fix-branch.yaml deleted file mode 100644 index 41744c7141333..0000000000000 --- a/.github/workflows/init-fix-branch.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Set up fix branch - -on: - workflow_dispatch: - inputs: - release_tag: - description: 'Exact release tag to base off (e.g. v2.14.9)' - required: true - type: string - fix_suffix: - description: 'Short name for this fix branch (e.g. fix-issue-123)' - required: true - type: string - -permissions: - contents: write - -jobs: - setup: - runs-on: ubuntu-latest - - steps: - - name: Checkout full repo (branches & tags) - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.18' - - - name: Run setup-fix - run: | - go run tools/fork-cli/main.go setup-fix \ - --release="${{ inputs.release_tag }}" \ - --fix-suffix="${{ inputs.fix_suffix }}" \ No newline at end of file diff --git a/.github/workflows/promote-fix-branch.yaml b/.github/workflows/promote-fix-branch.yaml deleted file mode 100644 index 437a4d971b9bd..0000000000000 --- a/.github/workflows/promote-fix-branch.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Promote Fix - -on: - workflow_dispatch: - inputs: - fix_branch: - description: 'Full fix branch (e.g. skyscanner-internal/releases/v2.14.9/fix-issue-123)' - required: true - type: string - proposal_branch: - description: 'Name under skyscanner-contrib/proposal/ (e.g. fix-issue-123)' - required: true - type: string - -permissions: - contents: write - -jobs: - promote: - runs-on: ubuntu-latest - - steps: - - name: Checkout full repo (branches & tags) - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.24.3' - - - name: Invoke promote-fix - run: | - go run tools/fork-cli/main.go promote-fix \ - --fix-branch="${{ inputs.fix_branch }}" \ - --proposal-branch="${{ inputs.proposal_branch }}" diff --git a/tools/fork-cli/INTERNAL_CONTRIB.md b/tools/fork-cli/INTERNAL_CONTRIB.md index ed2953c6d5579..4e2f267240dea 100644 --- a/tools/fork-cli/INTERNAL_CONTRIB.md +++ b/tools/fork-cli/INTERNAL_CONTRIB.md @@ -17,13 +17,12 @@ Our fork maintains several important branches: - **`skyscanner-internal/develop//`** - Release-pinned development branches based on upstream tags - - Format: `skyscanner-internal/develop/vX.Y.Z/fix-` - - Created by the **setup-fix** tool + - Format: `skyscanner-internal/develop/vX.Y.Z/fix-` (vX.Y.Z is the upstream release tag that should exist) + - Created by the **setup-fix** tool (don't create these manually) - Contains our CI folder copied from `skyscanner-internal/master` and fork tooling - **`skyscanner-contrib/master`** - Mirror of `argoproj/argo-cd:master` - - Kept in sync via automation - Never push to this directly - **`skyscanner-contrib/proposal/`** From ec36ba0fe6f0e4346d0e07d81f0d55e63b5d1166 Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Thu, 3 Jul 2025 15:12:49 +0100 Subject: [PATCH 4/9] fix: update main --- tools/fork-cli/main.go | 6 +++--- tools/fork-cli/main_test.go | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tools/fork-cli/main.go b/tools/fork-cli/main.go index 9a2ed9df39f55..f40968c497b2b 100644 --- a/tools/fork-cli/main.go +++ b/tools/fork-cli/main.go @@ -514,8 +514,8 @@ func workOnCmd(args []string, runner CommandRunner) int { runner.RunOrExit("git", "push", "-u", "origin", featureBranch) // Create PR automatically - title := fmt.Sprintf("feat: %s", *suffix) - body := fmt.Sprintf("Automated PR created via fork-cli\n\nUpdates VERSION to %s", newVersion) + title := "feat: " + *suffix + body := "Automated PR created via fork-cli\n\nUpdates VERSION to " + newVersion runner.RunOrExit("gh", "pr", "create", "--base", *devBranch, "--title", title, "--body", body) fmt.Println("βœ… PR created successfully!") @@ -528,7 +528,7 @@ func workOnCmd(args []string, runner CommandRunner) int { } func printConflictMessage(w io.Writer, releaseTag, fixBranch, proposal string) { - cmd := fmt.Sprintf("go run tools/fork-cli/main.go promote-fix --fix-branch=\"%s\" --proposal-branch=\"%s\"", + cmd := fmt.Sprintf("go run tools/fork-cli/main.go promote-fix --fix-branch=%q --proposal-branch=%q", fixBranch, proposal) fmt.Fprintf(w, "\n❌ Conflict detected during promotion of release %s.\n", releaseTag) fmt.Fprintln(w, "Please re-run this command locally to resolve interactively:") diff --git a/tools/fork-cli/main_test.go b/tools/fork-cli/main_test.go index 8eac0c8c8430d..729bb96cf6af9 100644 --- a/tools/fork-cli/main_test.go +++ b/tools/fork-cli/main_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "errors" "fmt" "os" @@ -19,7 +18,6 @@ type MockRunner struct { exitCalls []string branchesExist map[string]bool tagsExist map[string]bool - output *bytes.Buffer cmdIndex int hasUncommittedChanges bool branchUpToDate map[string]bool @@ -39,7 +37,6 @@ func NewMockRunner(t *testing.T) *MockRunner { expectedCmds: []expectedCmd{}, branchesExist: make(map[string]bool), tagsExist: make(map[string]bool), - output: &bytes.Buffer{}, branchUpToDate: make(map[string]bool), } } @@ -154,7 +151,7 @@ func (m *MockRunner) HasUncommittedChanges() bool { func (m *MockRunner) IsBranchUpToDate(localBranch, remoteBranch string) bool { upToDate, ok := m.branchUpToDate[localBranch] if !ok { - m.t.Logf("Branch up-to-date check not mocked for: %s", localBranch) + m.t.Logf("Branch up-to-date check not mocked for local branch %s and remote branch %s", localBranch, remoteBranch) return false } return upToDate @@ -337,8 +334,8 @@ func TestSyncForkCmd(t *testing.T) { // Set test environment oldToken := os.Getenv("GITHUB_TOKEN") - os.Setenv("GITHUB_TOKEN", "test-token") - defer os.Setenv("GITHUB_TOKEN", oldToken) + t.Setenv("GITHUB_TOKEN", "test-token") + defer t.Setenv("GITHUB_TOKEN", oldToken) // Run the command exitCode := syncForkCmd([]string{}, mock) From 8de9eb5491e63e5432ba4684200d5ff719bfe5c5 Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Fri, 4 Jul 2025 15:54:03 +0100 Subject: [PATCH 5/9] fix: test and lint --- tools/fork-cli/INTERNAL_CONTRIB.md | 2 ++ tools/fork-cli/main_test.go | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/tools/fork-cli/INTERNAL_CONTRIB.md b/tools/fork-cli/INTERNAL_CONTRIB.md index 4e2f267240dea..92c9dc902611a 100644 --- a/tools/fork-cli/INTERNAL_CONTRIB.md +++ b/tools/fork-cli/INTERNAL_CONTRIB.md @@ -77,6 +77,8 @@ Our fork maintains several important branches: 3. **Make changes and create PRs**: - Make changes, commit, and push your feature branch + - run `make pre-commit-local` to run pre commit checks (it's slow) + - make sure **all** commit messages follow the pattern `^(feat|fix|docs|test|ci|chore)!?(\\(.*\\))?!?:.*` - Create PRs against the development branch (not upstream!) - Use the `gh` CLI for convenience: ```shell diff --git a/tools/fork-cli/main_test.go b/tools/fork-cli/main_test.go index 729bb96cf6af9..7cba8595f5eea 100644 --- a/tools/fork-cli/main_test.go +++ b/tools/fork-cli/main_test.go @@ -266,9 +266,10 @@ func TestPromoteFixCmd(t *testing.T) { proposalFull := "skyscanner-contrib/proposal/" + proposalBranch mock.SetBranchUpToDate("skyscanner-contrib/master", true) + + // Expected command sequence: mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) - mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") mock.SetBranchExists(proposalFull, false) mock.ExpectCommand("git", "checkout", "-b", proposalFull) @@ -287,7 +288,6 @@ func TestPromoteFixCmd(t *testing.T) { mock.SetBranchUpToDate("skyscanner-contrib/master", false) mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) args := []string{"--fix-branch=" + fixBranch, "--proposal-branch=" + proposalBranch} exitCode := promoteFixCmd(args, mock) @@ -303,10 +303,8 @@ func TestPromoteFixCmd(t *testing.T) { mock.SetBranchUpToDate("skyscanner-contrib/master", true) mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) - mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") - mock.SetBranchExists(proposalFull, false) mock.ExpectCommand("git", "checkout", "-b", proposalFull) mock.ExpectCommand("git", "cherry-pick", "--keep-redundant-commits", "abcdef123456.."+fixBranch).WithError(errors.New("conflict")) @@ -450,10 +448,17 @@ func TestWorkOnCmd(t *testing.T) { mock := NewMockRunner(t) devBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" suffix := "add-logging" + featureBranch := fmt.Sprintf("%s-%s", devBranch, suffix) - mock.ExpectCommand("git", "fetch", "origin", devBranch+":"+devBranch) mock.ExpectCommand("git", "checkout", devBranch) - mock.ExpectCommand("git", "checkout", "-b", "feature/add-logging") + mock.ExpectCommand("git", "checkout", "-b", featureBranch) + mock.ExpectCommand("cat", "VERSION").WithOutput("v2.14.9") + mock.ExpectCommand("sh", "-c", "echo 'v2.14.9-add-logging' > VERSION") + mock.ExpectCommand("git", "add", "VERSION") + mock.ExpectCommand("git", "commit", "-m", "feat: add-logging\n\nUpdate VERSION to v2.14.9-add-logging") + mock.ExpectCommand("gh", "repo", "set-default", "Skyscanner/argo-cd") + mock.ExpectCommand("git", "push", "-u", "origin", "skyscanner-internal/develop/v2.14.9/fix-issue-123-add-logging") + mock.ExpectCommand("gh", "pr", "create", "--base", "skyscanner-internal/develop/v2.14.9/fix-issue-123", "--title", "feat: add-logging", "--body", "Automated PR created via fork-cli\n\nUpdates VERSION to v2.14.9-add-logging") exitCode := workOnCmd([]string{"--dev-branch=" + devBranch, "--suffix=" + suffix}, mock) assert.Equal(t, 0, exitCode) From a073460778733df195a08cd8f8ee64aaa2837ffa Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Fri, 4 Jul 2025 16:22:01 +0100 Subject: [PATCH 6/9] fix: test and lint --- tools/fork-cli/main.go | 6 +++--- tools/fork-cli/main_test.go | 30 ++++++++++++++++-------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tools/fork-cli/main.go b/tools/fork-cli/main.go index 9a2ed9df39f55..f40968c497b2b 100644 --- a/tools/fork-cli/main.go +++ b/tools/fork-cli/main.go @@ -514,8 +514,8 @@ func workOnCmd(args []string, runner CommandRunner) int { runner.RunOrExit("git", "push", "-u", "origin", featureBranch) // Create PR automatically - title := fmt.Sprintf("feat: %s", *suffix) - body := fmt.Sprintf("Automated PR created via fork-cli\n\nUpdates VERSION to %s", newVersion) + title := "feat: " + *suffix + body := "Automated PR created via fork-cli\n\nUpdates VERSION to " + newVersion runner.RunOrExit("gh", "pr", "create", "--base", *devBranch, "--title", title, "--body", body) fmt.Println("βœ… PR created successfully!") @@ -528,7 +528,7 @@ func workOnCmd(args []string, runner CommandRunner) int { } func printConflictMessage(w io.Writer, releaseTag, fixBranch, proposal string) { - cmd := fmt.Sprintf("go run tools/fork-cli/main.go promote-fix --fix-branch=\"%s\" --proposal-branch=\"%s\"", + cmd := fmt.Sprintf("go run tools/fork-cli/main.go promote-fix --fix-branch=%q --proposal-branch=%q", fixBranch, proposal) fmt.Fprintf(w, "\n❌ Conflict detected during promotion of release %s.\n", releaseTag) fmt.Fprintln(w, "Please re-run this command locally to resolve interactively:") diff --git a/tools/fork-cli/main_test.go b/tools/fork-cli/main_test.go index 8eac0c8c8430d..7cba8595f5eea 100644 --- a/tools/fork-cli/main_test.go +++ b/tools/fork-cli/main_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "errors" "fmt" "os" @@ -19,7 +18,6 @@ type MockRunner struct { exitCalls []string branchesExist map[string]bool tagsExist map[string]bool - output *bytes.Buffer cmdIndex int hasUncommittedChanges bool branchUpToDate map[string]bool @@ -39,7 +37,6 @@ func NewMockRunner(t *testing.T) *MockRunner { expectedCmds: []expectedCmd{}, branchesExist: make(map[string]bool), tagsExist: make(map[string]bool), - output: &bytes.Buffer{}, branchUpToDate: make(map[string]bool), } } @@ -154,7 +151,7 @@ func (m *MockRunner) HasUncommittedChanges() bool { func (m *MockRunner) IsBranchUpToDate(localBranch, remoteBranch string) bool { upToDate, ok := m.branchUpToDate[localBranch] if !ok { - m.t.Logf("Branch up-to-date check not mocked for: %s", localBranch) + m.t.Logf("Branch up-to-date check not mocked for local branch %s and remote branch %s", localBranch, remoteBranch) return false } return upToDate @@ -269,9 +266,10 @@ func TestPromoteFixCmd(t *testing.T) { proposalFull := "skyscanner-contrib/proposal/" + proposalBranch mock.SetBranchUpToDate("skyscanner-contrib/master", true) + + // Expected command sequence: mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) - mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") mock.SetBranchExists(proposalFull, false) mock.ExpectCommand("git", "checkout", "-b", proposalFull) @@ -290,7 +288,6 @@ func TestPromoteFixCmd(t *testing.T) { mock.SetBranchUpToDate("skyscanner-contrib/master", false) mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) args := []string{"--fix-branch=" + fixBranch, "--proposal-branch=" + proposalBranch} exitCode := promoteFixCmd(args, mock) @@ -306,10 +303,8 @@ func TestPromoteFixCmd(t *testing.T) { mock.SetBranchUpToDate("skyscanner-contrib/master", true) mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "fetch", "origin", fixBranch+":"+fixBranch) - mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") - mock.SetBranchExists(proposalFull, false) mock.ExpectCommand("git", "checkout", "-b", proposalFull) mock.ExpectCommand("git", "cherry-pick", "--keep-redundant-commits", "abcdef123456.."+fixBranch).WithError(errors.New("conflict")) @@ -337,8 +332,8 @@ func TestSyncForkCmd(t *testing.T) { // Set test environment oldToken := os.Getenv("GITHUB_TOKEN") - os.Setenv("GITHUB_TOKEN", "test-token") - defer os.Setenv("GITHUB_TOKEN", oldToken) + t.Setenv("GITHUB_TOKEN", "test-token") + defer t.Setenv("GITHUB_TOKEN", oldToken) // Run the command exitCode := syncForkCmd([]string{}, mock) @@ -453,10 +448,17 @@ func TestWorkOnCmd(t *testing.T) { mock := NewMockRunner(t) devBranch := "skyscanner-internal/develop/v2.14.9/fix-issue-123" suffix := "add-logging" + featureBranch := fmt.Sprintf("%s-%s", devBranch, suffix) - mock.ExpectCommand("git", "fetch", "origin", devBranch+":"+devBranch) mock.ExpectCommand("git", "checkout", devBranch) - mock.ExpectCommand("git", "checkout", "-b", "feature/add-logging") + mock.ExpectCommand("git", "checkout", "-b", featureBranch) + mock.ExpectCommand("cat", "VERSION").WithOutput("v2.14.9") + mock.ExpectCommand("sh", "-c", "echo 'v2.14.9-add-logging' > VERSION") + mock.ExpectCommand("git", "add", "VERSION") + mock.ExpectCommand("git", "commit", "-m", "feat: add-logging\n\nUpdate VERSION to v2.14.9-add-logging") + mock.ExpectCommand("gh", "repo", "set-default", "Skyscanner/argo-cd") + mock.ExpectCommand("git", "push", "-u", "origin", "skyscanner-internal/develop/v2.14.9/fix-issue-123-add-logging") + mock.ExpectCommand("gh", "pr", "create", "--base", "skyscanner-internal/develop/v2.14.9/fix-issue-123", "--title", "feat: add-logging", "--body", "Automated PR created via fork-cli\n\nUpdates VERSION to v2.14.9-add-logging") exitCode := workOnCmd([]string{"--dev-branch=" + devBranch, "--suffix=" + suffix}, mock) assert.Equal(t, 0, exitCode) From c359fa51e12a90d6e6aa8bc6940e4626b561c4be Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Fri, 4 Jul 2025 17:27:13 +0100 Subject: [PATCH 7/9] fix: test --- tools/fork-cli/main.go | 2 +- tools/fork-cli/main_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/fork-cli/main.go b/tools/fork-cli/main.go index f40968c497b2b..8d1d54c3a071b 100644 --- a/tools/fork-cli/main.go +++ b/tools/fork-cli/main.go @@ -314,7 +314,7 @@ func promoteFixCmd(args []string, runner CommandRunner) int { return 1 } - baseRelease := parts[2] // e.g. v2.9.14 + baseRelease := strings.Join(parts[:3], "/") // e.g. skyscanner-internal/develop/v2.9.14 // 3) Compute merge-base & commit-range gb := strings.TrimSpace(runner.RunAndCaptureOrExit("git", "merge-base", *fixBranch, baseRelease)) diff --git a/tools/fork-cli/main_test.go b/tools/fork-cli/main_test.go index 7cba8595f5eea..71bdc381d76d1 100644 --- a/tools/fork-cli/main_test.go +++ b/tools/fork-cli/main_test.go @@ -269,7 +269,7 @@ func TestPromoteFixCmd(t *testing.T) { // Expected command sequence: mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") mock.SetBranchExists(proposalFull, false) mock.ExpectCommand("git", "checkout", "-b", proposalFull) @@ -303,7 +303,7 @@ func TestPromoteFixCmd(t *testing.T) { mock.SetBranchUpToDate("skyscanner-contrib/master", true) mock.ExpectCommand("git", "fetch", "origin", "skyscanner-contrib/master:skyscanner-contrib/master") - mock.ExpectCommand("git", "merge-base", fixBranch, "v2.14.9").WithOutput("abcdef123456") + mock.ExpectCommand("git", "merge-base", fixBranch, "skyscanner-internal/develop/v2.14.9").WithOutput("abcdef123456") mock.ExpectCommand("git", "checkout", "skyscanner-contrib/master") mock.ExpectCommand("git", "checkout", "-b", proposalFull) mock.ExpectCommand("git", "cherry-pick", "--keep-redundant-commits", "abcdef123456.."+fixBranch).WithError(errors.New("conflict")) From 77d51de423808753e210692a2e85b6d030aa16ec Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Mon, 7 Jul 2025 08:44:38 +0100 Subject: [PATCH 8/9] chore: update docs --- tools/fork-cli/INTERNAL_CONTRIB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fork-cli/INTERNAL_CONTRIB.md b/tools/fork-cli/INTERNAL_CONTRIB.md index 8a9d86057077e..a01f78110ba85 100644 --- a/tools/fork-cli/INTERNAL_CONTRIB.md +++ b/tools/fork-cli/INTERNAL_CONTRIB.md @@ -40,7 +40,7 @@ Our fork maintains several important branches: # If you have changes: git stash push -m "WIP before sync" # or - git add . && git commit -m "WIP: save local changes" + git add . && git commit -m "(chore(WIP): save local changes" ``` 2. **Sync our fork with upstream**: From 493ce321b51184f4b4f5b32620ad141f7e304d79 Mon Sep 17 00:00:00 2001 From: Harshit-Bajpai Date: Mon, 7 Jul 2025 08:46:45 +0100 Subject: [PATCH 9/9] chore: update docs --- tools/fork-cli/INTERNAL_CONTRIB.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/fork-cli/INTERNAL_CONTRIB.md b/tools/fork-cli/INTERNAL_CONTRIB.md index a01f78110ba85..ab849f14bc032 100644 --- a/tools/fork-cli/INTERNAL_CONTRIB.md +++ b/tools/fork-cli/INTERNAL_CONTRIB.md @@ -40,7 +40,7 @@ Our fork maintains several important branches: # If you have changes: git stash push -m "WIP before sync" # or - git add . && git commit -m "(chore(WIP): save local changes" + git add . && git commit -m "chore(WIP): save local changes" ``` 2. **Sync our fork with upstream**: