Apresentando .discourse-compatibility: versões fixas de plugins/temas para versões mais antigas do Discourse

Olá, pessoal :wave:, acabei de mesclar um novo recurso que ajudará plugins e temas a fixar versões específicas quando instalados em instâncias mais antigas do Discourse.

Agora é possível incluir um arquivo .discourse-compatibility na raiz de um repositório de plugin ou tema, que define qual versão deve ser verificada ao instalar em versões mais antigas do Discourse.


Fundamentação

É chato ter que lembrar quais plugins e temas são compatíveis com quais versões do Discourse. Como administrador, deve ser possível examinar facilmente essas alterações e encontrar uma versão adequada para sua instalação do Discourse, sem precisar ler o histórico de commits do plugin. Como autor de plugin ou tema, deve ser possível gerenciar as versões de instalação enquanto se fazem alterações incompatíveis com versões anteriores, para não quebrar instalações mais antigas.

As atualizações do software Discourse são lançadas com bastante rapidez, o que, embora seja incrível, torna a manutenção de instâncias do Discourse com muitos plugins às vezes muito difícil, especialmente se você estiver seguindo outros ciclos de lançamento/versões, como a versão estável atual. Meu plano aqui é permitir um ecossistema que facilite o processo de atualização para quem segue a versão estável ou outro ciclo de lançamento, e oferecer aos administradores do site um método para buscar rápida e automaticamente qualquer versão de plugin compatível com a versão do Discourse que estão mirando.

Anúncio Original (agora substituído pela documentação vinculada acima)

Implementação

Primeira coisa a notar: estamos dependendo das tags do núcleo do Discourse aqui, já que as versões beta do Discourse são lançadas com frequência suficiente para que possamos fixar versões de plugins contra elas. Fixar versões contra hashes do git é um pesadelo por muitos motivos, então podemos usar o git describe para obter a tag beta/estável mais próxima.

Em um plugin ou tema, agora suportamos um arquivo de compatibilidade de versão chamado .discourse-compatibility na raiz. Este arquivo é uma lista ordenada decrescente (versões mais recentes do Discourse primeiro) que especifica um mapa de compatibilidade.

Exemplo

2.5.0.beta2: git-hash-1234e5f5d
2.4.4.beta6: 4444ffff33dd
2.4.2.beta1: named-git-tag-or-branch

Para cada plugin/tema, uma atualização ou reconstrução continuará a verificar um commit/branch/tag nomeado posterior até encontrar um que seja igual ou posterior à versão atual do Discourse.
Por exemplo, para o arquivo de versão acima, se a versão atual do Discourse for 2.4.6.beta12, ele irá escanear o arquivo e escolher a entrada para 2.5.0.beta2.

Se a versão atual do Discourse for 2.4.4.beta6, ele escolherá a entrada correspondente para 2.4.4.beta6.

Se não houver versão posterior, ele permanece na versão atual verificada.
Por exemplo, para 2.5.0.beta3, nenhuma fixação ocorreria.

Se não houver versão anterior, ele verifica a mais antiga listada no arquivo de versão.
Por exemplo, para 2.2.1.beta22, ele verificaria a mais antiga possível dada a “versão”, a entrada para 2.4.2.beta1.


O objetivo aqui é aliviar a dor de manter implantações alternativas que não estejam estritamente em “tests-passed” no futuro e oferecer flexibilidade aos administradores sobre quando e onde atualizar. Estamos fazendo isso permitindo que autores de plugins e temas tenham uma maneira de desenvolver alterações incompatíveis com versões anteriores sem afetar instalações em versões mais antigas do Discourse.

50 curtidas

This is a great feature, thank you :slight_smile:

I like the directionality of this, i.e. it makes it possible to manage this issue from within the plugin itself, not requiring the site admin to do anything per se.

I have a few initial questions:

  • Will the existing required_version plugin metadata check in plugin activation remain? And how do you see that interelating with this (if at all)?

  • I see this is added in the form of a rake task atm. How does it relate to, what is the intended use, with discourse_docker (i.e. the launcher) and docker_manager? I see you’ve made additions to both repos, but could you explain how it’s intended to work in both environments?

12 curtidas

Yeah, that’s the idea - make it so plugin authors have the capability to add backwards compatibility so that admins don’t need to worry!

There are currently no plans to change or remove the required_version plugin metadata. They’re related, but are still separate in my mind - the required_version min/max fullstop disallows the plugin from being installed by throwing an error, and is loaded after this compatibility pull. If you want to prevent ancient Discourse instances from using your plugin, I’d say it’s still a good idea to include required_version for the first minimum version - no amount of compatibility hunting is going to fix that :wink:

There shouldn’t be any config changes needed in normal use, it’ll pick up changes automatically. The rake task is called in discourse_docker after the plugins are cloned, so in the launcher order:

  • Plugins cloned
  • Compatibility checked, and checked out (if applicable)
  • migrate

In docker_manager, older Discourse versions will be able to update plugins to a compatible version. The UI here stays the same, but an “up to date” plugin is now according to the compatibility file.

TLDR, no changes are needed for either use-case to start taking advantage of this, if that’s what you’re wondering.

Under the hood, it uses git show HEAD@{upstream}:.discourse-compatibility to read the latest file, and git reset --hard #{checkout_version} to checkout the correct version. That way we are able to use the latest compatibility, and older Discourse versions will not be stuck on an old (possible invalid) compat file.

11 curtidas

Cool, thanks for explaining that.

So I poured myself a :wine_glass: and gave this a shot with the Custom Wizard Plugin.

I checked each tag out one by one in reverse order starting with v2.6.0.beta1. I found these git commands helpful:

git tag --list \\ e.g. git tag --list 'v2.5.0*'
git checkout tags/tag \\ e.g. git checkout tags/v2.5.0.beta7

It didn’t take too long to find a tag that wasn’t working with the current version of the plugin: v2.5.0.beta7 doesn’t include discourse/app/components/d-textarea which the custom wizard tries to import.

So, then I found the commit in the plugin that added that import, took the sha1 of the previous commit, checked that out and tested (worked fine), and added this to .discourse-compatibility:

v2.5.0.beta7: 802d74bab2ebe19a106f75275342dc2e9cc6066a

I then pushed that to a branch with the latest plugin code (a branch for testing, not necessary normally), and rebuilt a dockerized test server with that plugin branch and the version set at v2.5.0.beta7.

That didn’t work, then it hit me that, of course, the rake task plugin:pull_compatible_all doesn’t exist in v2.5.0.beta7, so this isn’t going to work retrospectively (I blame the :wine_glass:). Sure enough in the launcher logs I see

Don't know how to build task 'plugin:pull_compatible_all' (See the list of available tasks with `rake --tasks`)

Is that the gist of how you imagine this being used though?

On the required_version front, I encountered that here as the test server had the discourse-legal-tools plugin installed, which has a required_version of v2.5.0, so it initially failed on v2.5.0.beta7. I think I’ll transfer that plugin over to this new system. I can still see required_version being useful to set an absolute baseline as you say.

11 curtidas

Yeah that’s correct - because this isn’t baked into discourse core until now, it won’t work on anything older than 2.6.0.beta1 (currently). I still have to port this to stable + latest beta, so we’ll be able to use it against 2.5.0, but we’re not planning on porting it any earlier.

11 curtidas

Just a follow up here that I’ve now added a compatbility file to Custom Wizard master and will be using it to pin the plugin, including v2.6.0.beta1 (current beta) and v2.5.0 (current stable) when this is ported. See further:

https://meta.discourse.org/t/custom-wizard-plugin/73345/562?u=angus

5 curtidas

One note and a question here.

Firstly, just a note that the syntax for discourse version tags in the .discourse-compatibility file is 2.5.0 (like in @featheredtoast’s example), not v2.5.0. If you have a v you’ll get this error:

Malformed version number string v2.6.0.beta1

Secondly, @featheredtoast pointed out that the pinning system is now backported to stable. I missed this as I was looking at the v2.5.0 tag.

That raises a slight question for me. Discourse’s branches do not directly equate to discourse’s release tags. Most sites running older versions of discourse are probably on a branch, i.e. stable as opposed to a release, i.e. v2.5.0.

If a breaking change (for a plugin) is also added to an “older” branch, i.e. stable, what does this mean for the pins? I probably just don’t fully understand how the versioning system works.

6 curtidas

Versions correspond to the discourse version defined in the app, not the git tag.

So “2.5.0” in this context will mean any version declared as 2.5.0, up to the new version bump of 2.5.1 (or 2.6.0.beta1).

If a breaking change (for a plugin) is also added to the stable branch, that would indeed also break the plugin. The entire point of versioning is so we can be relatively sure we aren’t introducing breaking changes to that version, so that’s something to scream about separately. Our intent for stable is to backport only security and critical fixes (and minor features, when appropriate).

The intent of this is so we have the ability to calculate older working versions of plugins against older working versions of Discourse. This is by no means a silver bullet, but it does give us a platform in which to do it, if we are actually careful about choosing what to backport.

7 curtidas

This mostly works, unless you’re cloning with a --depth=1.

I’ll implement a hook to call a git fetch --depth 1 {upstream} commit soon if the target cannot be found initially, otherwise we’d be stuck doing a partial/full clone which is not ideal. We should be able to fetch what we need.

Edit: Updated here:

I’ve backported this one to Beta and Stable as well - so we should be able to pin even with shallow clones now.

6 curtidas

Apologies in advance as I often turn to this somewhat late in the night, so I’m not sure if I’m 100% correct here, and this may well be something you’re aware of already. I’m memorialising it here partly for my own sanity as it’s tripped me up a couple of times now.

I think this mechanism is effectively unworkable for the beta branch if the plugin is used on an instance you don’t control (i.e. it’s open source) that’s being updated in the common fashion. The common fashion of updating is for the site admin to do so when prompted in the admin UI.

Given this

And this

And that tests-passed and beta have the same Discourse version but not the same code, e.g. both are currently 2.6.0.beta2:

This follows:

  1. To support the beta branch you need to pin a commit to the latest beta release, as that is the one sites on the beta branch will be using.

  2. However, if the latest beta release is in the compatability file, instances running tests-passed will also use the pinned commit.

This means you can’t support the standard usage of both tests-passed and beta at the same time in an open souce plugin. Given that the majority of people who install plugins are on tests-passed, you effectively can’t support beta via this method.

Note that it does work in practice for the stable branch as stable has a different Discourse version from beta and tests-passed.

4 curtidas

Oh yeah, this is something I am aware of, it doesn’t truly resolve until the next beta cut. We should definitely also find a way to distinguish between beta and tests passed versions for this.

When I was setting the feature up, I had stable and weird forks in mind, and didn’t see until later that latest and stable were sharing versions.

6 curtidas

Thanks for confirming.

It seems the effective upshot is that the process should not run if the instance is running tests-passed. You can’t include the tests-passed version in the file for the reasons described above, so precluding the process on tests-passed would not change its current behaviour.

One way to implement this would be

def self.find_compatible_resource(version_list, version = ::Discourse::VERSION::STRING)
 
   return if Discourse.git_branch === 'tests-passed'

   ...
end

This would make it possible to use the latest beta version in the file for the purposes of pinning plugins for sites running beta. And you could continue supporting tests-passed in the latest commit of the plugin, i.e. without using this file. If you’re on board with that I can make a PR.

Alternatively, I feel like the underlying issue here is this

I’m sure this is something you guys have discussed before, but would it be possible to do something like

  • tests-passed: 2.6.0.tests-passed, i.e. set the PRE as tests-passed on the tests-passed branch.

  • beta: 2.6.0.beta2, i.e. as it is currently

@jomaxro Could you help me understand this one?

7 curtidas

yeah, I am in favor of adding an additional marking for pre-beta release vs an actual beta release, provided it’s easy enough to do. That would help resolve the underlying issue here. I’ve seen other software projects bump the version to the next version before its release (eg after releasing beta1, bump the “version” to beta2).

Traditionally though Discourse has not done that so it depends on what method is easiest for the project to adopt.

6 curtidas

Encontrei outro problema.

Esta alteração introduz $danger-low-mid na versão 2.6.0beta2.

Isso quebrou o plugin discourse-styleguide, que foi atualizado, e um arquivo .discourse-compatibility foi criado para manter o plugin no commit anterior para o Discourse 2.5.0.

Isso causa falhas no Discourse 2.5.1. Como essa alteração nunca será portada de volta para a versão estável, o arquivo discourse-compatibility precisaria ser atualizado a cada nova versão estável 2.5.x.

Cada arquivo discourse-compatibility de cada plugin precisaria ser atualizado a cada nova versão estável 2.5.x.

Alternativamente, o arquivo de compatibilidade poderia referenciar a versão 2.5.999, mantendo efetivamente o plugin no commit 1f86468b2c81b40e97f1bcd16ea4bb780634e2c7 durante toda a vida útil da série 2.5.x. Mas isso me parece muito hacky.

6 curtidas

Parece seguro travar na versão 2.6.0beta1, o que também seria mais correto, já que o problema foi introduzido na beta2. Faz sentido? Isso também cobriria todas as versões 2.5.

Sua solução “gambiarra” de 2.5.999 parece ser uma boa alternativa (embora bagunçada, como você mencionou). Tentei manter o travamento aqui o mais simples possível, mas você tem razão ao dizer que ainda não temos uma maneira real de declarar 2.5.x como uma versão travada válida.

Outra alternativa para esse caso específico de travar na última versão estável, mas mantendo as betas da próxima versão sem travamento, que pode ser um pouco mais sutil, mas me parece muito menos gambiarra para travar toda a branch 2.5.x, é fazer o travamento em 2.6.0.beta0 ou 2.6.0.alpha1, o que semanticamente significa travar tudo antes e até a versão entre 2.5.x e 2.6.0.beta1. Isso significa:

Tudo da versão 2.5.x está coberto pelo travamento.
Tudo da versão 2.6.0.beta(1+) continua sem travamento.

6 curtidas

Sim, para mim também parece menos hacky.

Mas tanto a solução 2.5.999 quanto a 2.6.0beta0 não cobrem um caso semelhante: e se um problema fosse introduzido na 2.6.0beta3 e fosse backportado para a 2.5.2?

Mais casos extremos, mais recursos ocultos: você pode realmente “desfazer” o pin com uma entrada em branco:

Com um arquivo de compatibilidade de:

        2.6.0.beta1: twofiveall
        2.4.4.beta6: ~
        2.4.2.beta1: twofourtwobetaone

Qualquer versão entre 2.4.2.beta1 e 2.4.4.beta6 não será fixada (pinned). Qualquer versão posterior, até 2.6.0.beta1, será fixada. Após isso, será desfixada novamente.

Internamente, qualquer coisa que seja avaliada como nil permanecerá inalterada ou na versão mais recente. Uma entrada em branco ou ~ é avaliada como nil (por meio do analisador YAML do Ruby), o que contorna a função de fixação.

8 curtidas

É possível usar um plugin de versão antiga em uma instância auto-hospedada?

1 curtida

Sim, você pode ajustar os comandos git clone em seu app.yml para clonar a versão que desejar. Observe que você precisa garantir que o Discourse também esteja fixado na versão desejada. E você também pode precisar fixar o discourse_docker a uma versão compatível com a versão do Discourse que você está executando. Se você estiver fazendo tudo isso, é mais fácil não atualizar.

2 curtidas