Skip to content
Snippets Groups Projects
Select Git revision
  • 18e94c974ecc8a4474335dee855e44b4025cdee5
  • master default protected
  • 366-signe-a-cote-du-droit-en-vigueur-sur-l-ui-pour-indiquer-que-la-reforme-a-eu-lieu-mais-qu-elle-n
  • revalo_retraites
  • 381-pb-affichage-labels-des-parametres-sur-plus-de-3-lignes
  • ajoute-duplicate-aide-logement
  • poc_castype_ia
  • parametres-editables-budget
  • ui-parametres
  • 355-les-dispositifs-prestations-sociales-du-graphique-se-cachent-montrent-en-meme-temps-2
  • 358-les-variables-dont-le-montant-est-nul-apparaissent-en-bleu-et-non-cliquables
  • 356-ajuster-la-largeur-sur-les-graphiques-budgetaires
  • incoherence_cas_type_0
  • fix-ui-suppression-tranches-baremes
  • ajout-agregat-cehr-version-plf
  • impact_carbone
  • xlsx
  • header_revamp
  • 270-concevoir-la-page-d-accueil-leximpact
  • 219-conversion-des-montants-min-et-max-de-l-axe-des-x-en-smic
  • 294-afficher-le-salaire-des-cas-types-en-nombre-de-smic
  • 0.0.1174
  • 0.0.1173
  • 0.0.1172
  • 0.0.1171
  • 0.0.1170
  • 0.0.1169
  • 0.0.1168
  • 0.0.1167
  • 0.0.1166
  • 0.0.1165
  • 0.0.1164
  • 0.0.1163
  • 0.0.1162
  • 0.0.1161
  • 0.0.1160
  • 0.0.1159
  • 0.0.1158
  • 0.0.1157
  • 0.0.1156
  • 0.0.1155
41 results

parameters.ts

Blame
  • gitlab-ci.ts 13.22 KiB
    import assert from "assert"
    import { $, fetch, fs } from "zx"
    
    interface Package {
      name: string
      version: string
    }
    
    export interface VersionObject {
      major: number
      minor: number
      patch: number
    }
    
    const {
      CI_COMMIT_BRANCH,
      CI_COMMIT_TAG,
      CI_COMMIT_TITLE,
      CI_DEFAULT_BRANCH,
      CI_JOB_TOKEN,
      CI_PIPELINE_SOURCE,
      CI_PROJECT_NAME,
      CI_PROJECT_NAMESPACE,
      CI_SERVER_HOST,
      CI_SERVER_URL,
      // NPM_EMAIL,
      // NPM_TOKEN,
      SSH_KNOWN_HOSTS,
      SSH_PRIVATE_KEY,
    } = process.env
    const packagePath = "package.json"
    
    function checkVersionObject(
      {
        major: majorReference,
        minor: minorReference,
        patch: patchReference,
      }: VersionObject,
      versionObject: VersionObject | undefined,
    ): boolean {
      if (versionObject === undefined) {
        return true
      }
      const { major, minor, patch } = versionObject
      if (major < majorReference) {
        return true
      }
      if (major === majorReference) {
        if (minor < minorReference) {
          return true
        }
        if (minor === minorReference) {
          return patch <= patchReference || patch === patchReference + 1
        }
        return minor === minorReference + 1 && patch === 0
      }
      return major === majorReference + 1 && minor === 0 && patch === 0
    }
    
    async function commitAndPushWithUpdatedVersions(
      nextVersionObject?: VersionObject | undefined,
    ) {
      if (nextVersionObject === undefined) {
        // Retrieve next version of project.
        nextVersionObject = await latestVersionObjectFromTags()
        assert.notStrictEqual(nextVersionObject, undefined)
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        nextVersionObject!.patch++
      }
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const nextVersion = versionFromObject(nextVersionObject!)
    
      if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
        let packageJson = await fs.readFile(packagePath, "utf-8")
        packageJson = packageJson.replace(
          /^ {2}"version": "(.*?)",$/m,
          `  "version": "${nextVersion}",`,
        )
        await fs.writeFile(packagePath, packageJson, "utf-8")
      }
    
      await $`git add .`
      if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
        await $`git commit -m ${nextVersion}`
        await $`git push --set-upstream`
      }
      await $`git tag -a ${nextVersion} -m ${nextVersion}`
      await $`git push --set-upstream --tags`
    
      // // Note: Don't fail when there is nothing new to publish.
      // if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
      //   await nothrow($`npm publish`)
      // }
    }
    
    async function configureGit() {
      console.log("Configuring git…")
    
      // Set the Git user name and email.
      await $`git config --global user.email "admin+leximpact-socio-fiscal-ui-ci@tax-benefit.org"`
      await $`git config --global user.name "Leximpact socio-fiscal UI CI"`
    
      console.log("Git configuration completed.")
    }
    
    // async function configureNpm() {
    //   console.log("Configuring npm…")
    
    //   // Configure npm to be able to publish packages.
    //   if (NPM_EMAIL !== undefined && NPM_TOKEN !== undefined) {
    //     await $`echo ${`//registry.npmjs.org/:_authToken=${NPM_TOKEN}`} > ~/.npmrc`
    //     await $`echo ${`email=${NPM_EMAIL}`} >> ~/.npmrc`
    //   }
    
    //   console.log("Npm configuration completed.")
    // }
    
    async function configureSsh() {
      console.log("Configuring ssh…")
    
      // Note: `eval $(ssh-agent -s)` must be done before calling this script because it
      // creates 2 environment variables (then used by ssh clients).
      // // Install ssh-agent if not already installed, to be able to use git with ssh.
      // if ((await $`which ssh-agent`.exitCode) !== 0) {
      //   await $`apt install -y openssh-client`
      // }
      // // Run ssh-agent (inside the build environment)
      // await $`eval $(ssh-agent -s)`
    
      // Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
      // Use `tr` to fix line endings which makes ed25519 keys work without
      // extra base64 encoding.
      // https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
      if (SSH_PRIVATE_KEY !== undefined) {
        await $`echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -`
      }
    
      if (SSH_KNOWN_HOSTS !== undefined) {
        // Create the SSH directory and give it the right permissions
        await $`mkdir -p ~/.ssh`
        await $`chmod 700 ~/.ssh`
        // Accept the SSH host keys of GitLab server.
        await $`echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts`
        await $`chmod 644 ~/.ssh/known_hosts`
      }
    
      console.log("Ssh configuration completed.")
    }
    
    async function latestVersionObjectFromTags(): Promise<
      VersionObject | undefined
    > {
      // Retrieve all tags.
      await $`git fetch --all --tags`
    
      return (await $`git tag --list`).stdout
        .split("\n")
        .map(objectFromVersion)
        .filter((versionObject) => versionObject !== undefined)
        .sort((versionObject1, versionObject2) =>
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          versionObject1!.major !== versionObject2!.major
            ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              versionObject2!.major - versionObject1!.major
            : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
              versionObject1!.minor !== versionObject2!.minor
              ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                versionObject2!.minor - versionObject1!.minor
              : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                versionObject2!.patch - versionObject1!.patch,
        )[0]
    }
    
    async function main() {
      console.log("Starting gitlab-ci.ts…")
    
      await configureGit()
      // await configureNpm()
      await configureSsh()
    
      console.log(`Handling "${CI_PIPELINE_SOURCE}" event…`)
      switch (CI_PIPELINE_SOURCE) {
        case "api":
        case "pipeline":
        case "schedule":
        case "trigger":
        case "web": {
          if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
            // A pipeline has been triggered (for example when OpenFisca-France has been updated or a user as
            // launched a new pipeline manually or…).
            // => A new version of project must be created if and only if a dependency has
            // changed.
    
            console.log(`Handling trigger…`)
    
            await resetGitRepository()
    
            // Use the latest version of @leximpact/socio-fiscal-openfisca-json
            await $`npm install @leximpact/socio-fiscal-openfisca-json@latest`
    
            // Test project with the current dependencies (and latest version of @leximpact/socio-fiscal-openfisca-json).
            await $`ln -s example.env .env`
            await $`npm run build`
            // await $`npm test`
    
            await $`git add .`
            if ((await $`git diff --quiet --staged`.exitCode) !== 0) {
              // `npm install @leximpact/socio-fiscal-openfisca-json@latest` has updated `package-lock.json`.
              // => Generate a new version of leximpact-socio-fiscal-api.
              const pkg = await fs.readJson(packagePath)
              const nextVersionObject =
                await nextVersionObjectFromPackageAndTags(pkg)
              await commitAndPushWithUpdatedVersions(nextVersionObject)
              await triggerDevDeployPipeline()
              await triggerProdDeployPipeline()
            }
          } else {
            console.log(
              `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
            )
          }
          break
        }
        case "push": {
          if (CI_COMMIT_BRANCH !== undefined) {
            console.log(`Push to branch ${CI_COMMIT_BRANCH}`)
    
            if (CI_COMMIT_BRANCH.match(/^\d+_\d+_\d+$/) != null) {
              console.log(
                `Ignoring commit in version branch (for branch ${CI_COMMIT_BRANCH}).`,
              )
            } else if (CI_COMMIT_TITLE?.match(/^\d+\.\d+\.\d+( |$)/) != null) {
              console.log(
                `Ignoring version commit (for version ${CI_COMMIT_TITLE}).`,
              )
            } else {
              await resetGitRepository()
    
              // Use the latest version of @leximpact/socio-fiscal-openfisca-json
              await $`npm install @leximpact/socio-fiscal-openfisca-json@latest`
    
              const pkg = await fs.readJson(packagePath)
              const nextVersionObject =
                await nextVersionObjectFromPackageAndTags(pkg)
    
              // Test project with the current dependencies (and latest @leximpact/socio-fiscal-openfisca-json).
              await $`ln -s example.env .env`
              await $`npm run build`
              // await $`npm test`
    
              if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
                // A merge request has been merged into master (ie content of project has been changed).
                // => Create a new version of project.
                await commitAndPushWithUpdatedVersions(nextVersionObject)
                await triggerDevDeployPipeline()
                await triggerProdDeployPipeline()
              }
            }
          } else if (CI_COMMIT_TAG !== undefined) {
            console.log(`Pushing commit tag "${CI_COMMIT_TAG}"…`)
          } else {
            console.log(`Unhandled push event.`)
          }
          break
        }
        default:
          console.log(
            `Unhandled event "${CI_PIPELINE_SOURCE}" in branch "${CI_COMMIT_BRANCH}".`,
          )
      }
    }
    
    function maxVersionObject(
      versionObject1: VersionObject,
      versionObject2: VersionObject | undefined,
    ): VersionObject {
      if (versionObject2 === undefined) {
        return versionObject1
      }
      const { major: major1, minor: minor1, patch: patch1 } = versionObject1
      const { major: major2, minor: minor2, patch: patch2 } = versionObject2
      if (major1 < major2) {
        return versionObject2
      }
      if (major1 > major2) {
        return versionObject1
      }
      if (minor1 < minor2) {
        return versionObject2
      }
      if (minor1 > minor2) {
        return versionObject1
      }
      if (patch1 < patch2) {
        return versionObject2
      }
      if (patch1 > patch2) {
        return versionObject1
      }
      // Both versions are the same. Return one of them.
      return versionObject1
    }
    
    async function nextVersionObjectFromPackageAndTags(
      pkg: Package,
    ): Promise<VersionObject> {
      // Retrieve current version of project.
      const tagVersionObject = (await latestVersionObjectFromTags()) ?? {
        major: 0,
        minor: 0,
        patch: 0,
      }
      const tagVersion = versionFromObject(tagVersionObject)
      let nextVersionObject = {
        ...tagVersionObject,
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        patch: tagVersionObject!.patch + 1,
      }
    
      // Ensure that the version numbers of project packages are
      // compatible with last version tag.
      const { version: packageVersion } = pkg
      const packageVersionObject = objectFromVersion(packageVersion)
      assert(
        checkVersionObject(tagVersionObject, packageVersionObject),
        `In ${packagePath}, project version should be compatible with ${tagVersion}, got "${packageVersion}" instead.`,
      )
      nextVersionObject = maxVersionObject(nextVersionObject, packageVersionObject)
    
      return nextVersionObject
    }
    
    function objectFromVersion(
      version: string | undefined,
    ): VersionObject | undefined {
      if (version === undefined) {
        return undefined
      }
      const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/) as string[]
      if (match === null) {
        return undefined
      }
      return {
        major: parseInt(match[1]),
        minor: parseInt(match[2]),
        patch: parseInt(match[3]),
      }
    }
    
    async function resetGitRepository() {
      // Reset current repo, because it may have been tranformed by a previous CI that failed.
      await $`git reset --hard`
      await $`git switch ${CI_COMMIT_BRANCH}`
      await $`git fetch origin ${CI_COMMIT_BRANCH}`
      await $`git reset --hard origin/${CI_COMMIT_BRANCH}`
      await $`git status`
      if (CI_COMMIT_BRANCH === CI_DEFAULT_BRANCH) {
        await $`git remote set-url origin git@${CI_SERVER_HOST}:${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
      } else {
        await $`git remote set-url origin https://${CI_SERVER_HOST}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}.git`
      }
    }
    
    async function triggerDevDeployPipeline() {
      const urlString = new URL(
        `/api/v4/projects/28/trigger/pipeline`,
        CI_SERVER_URL,
      ).toString()
      console.log(
        "Triggering LexImpact socio-fiscal Dev Deploy pipeline on master branch…",
      )
      const response = await fetch(urlString, {
        body: new URLSearchParams({
          ref: "master",
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          token: CI_JOB_TOKEN!,
        }).toString(),
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        method: "POST",
      })
      assert(
        response.ok,
        `Unexpected response from ${urlString}: ${response.status} ${
          response.statusText
        }\n${await response.text()}`,
      )
    }
    
    async function triggerProdDeployPipeline() {
      const urlString = new URL(
        `/api/v4/projects/29/trigger/pipeline`,
        CI_SERVER_URL,
      ).toString()
      console.log(
        "Triggering LexImpact socio-fiscal Prod Deploy pipeline on master branch…",
      )
      const response = await fetch(urlString, {
        body: new URLSearchParams({
          ref: "master",
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          token: CI_JOB_TOKEN!,
        }).toString(),
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
        method: "POST",
      })
      assert(
        response.ok,
        `Unexpected response from ${urlString}: ${response.status} ${
          response.statusText
        }\n${await response.text()}`,
      )
    }
    
    function versionFromObject({ major, minor, patch }: VersionObject): string {
      return `${major}.${minor}.${patch}`
    }
    
    main()
      .then(() => {
        process.exit(0)
      })
      .catch((error: unknown) => {
        console.error(error)
        process.exit(1)
      })